Issues (15)

Security Analysis    no vulnerabilities found

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  Header Injection
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

sources/folders.class.php (3 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This file is part of the TeamPass project.
9
 * 
10
 * TeamPass is free software: you can redistribute it and/or modify it
11
 * under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, version 3 of the License.
13
 * 
14
 * TeamPass is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 * 
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 * 
22
 * Certain components of this file may be under different licenses. For
23
 * details, see the `licenses` directory or individual file headers.
24
 * ---
25
 * @file      folders.class.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-2025 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
use TeampassClasses\NestedTree\NestedTree;
33
use TeampassClasses\SessionManager\SessionManager;
34
use TeampassClasses\ConfigManager\ConfigManager;
35
36
class FolderManager
37
{
38
    private $lang;
39
    private $settings;
40
41
    /**
42
     * Constructor
43
     */
44
    public function __construct($lang)
45
    {
46
        $this->lang = $lang;
47
        $this->loadSettings();
48
    }
49
50
    /**
51
     * Load settings
52
     */
53
    private function loadSettings()
54
    {
55
        // Load config if $SETTINGS not defined
56
        $configManager = new ConfigManager();
57
        $this->settings = $configManager->getAllSettings();
58
    }
59
60
    /**
61
     * Create a new folder
62
     *
63
     * @param array $params
64
     * @return array
65
     */
66
    public function createNewFolder(array $params, array $options = []): array
67
    {
68
            // Decompose parameters
69
            $title = $params['title'] ?? '';
70
            $parent_id = $params['parent_id'] ?? 0;
71
            $personal_folder = $params['personal_folder'] ?? 0;
72
            $complexity = $params['complexity'] ?? 0;
73
            $user_is_admin = $params['user_is_admin'] ?? 0;
74
            $user_accessible_folders = $params['user_accessible_folders'] ?? [];
75
            $user_can_create_root_folder = $params['user_can_create_root_folder'] ?? 0;
76
77
78
        if ($this->isTitleNumeric($title)) {
79
            return $this->errorResponse($this->lang->get('error_only_numbers_in_folder_name'));
80
        }
81
82
        if (!$this->isParentFolderAllowed($parent_id, $user_accessible_folders, $user_is_admin, $user_can_create_root_folder)) {
0 ignored issues
show
It seems like $user_is_admin can also be of type integer; however, parameter $user_is_admin of FolderManager::isParentFolderAllowed() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

82
        if (!$this->isParentFolderAllowed($parent_id, $user_accessible_folders, /** @scrutinizer ignore-type */ $user_is_admin, $user_can_create_root_folder)) {
Loading history...
It seems like $user_can_create_root_folder can also be of type integer; however, parameter $user_can_create_root_folder of FolderManager::isParentFolderAllowed() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

82
        if (!$this->isParentFolderAllowed($parent_id, $user_accessible_folders, $user_is_admin, /** @scrutinizer ignore-type */ $user_can_create_root_folder)) {
Loading history...
83
            return $this->errorResponse($this->lang->get('error_folder_not_allowed_for_this_user'));
84
        }
85
86
        if (!$this->checkDuplicateFolderAllowed($title) && $personal_folder == 0) {
87
            return $this->errorResponse($this->lang->get('error_group_exist'));
88
        }
89
90
        $parentFolderData = $this->getParentFolderData($parent_id);
91
92
        $parentComplexity = $this->checkComplexityLevel($parentFolderData, $complexity, $parent_id);
93
        if (isset($parentComplexity['error']) && $parentComplexity['error'] === true) {
94
            return $this->errorResponse($this->lang->get('error_folder_complexity_lower_than_top_folder') . " [<b>{$this->settings['TP_PW_COMPLEXITY'][$parentComplexity['valeur']][1]}</b>]");
95
        }
96
97
        return $this->createFolder($params, array_merge($parentFolderData, $parentComplexity), $options);
98
    }
99
100
    /**
101
     * Check if title is numeric
102
     *
103
     * @param string $title
104
     * @return boolean
105
     */
106
    private function isTitleNumeric($title)
107
    {
108
        return is_numeric($title);
109
    }
110
111
    /**
112
     * Check if parent folder is allowed
113
     *
114
     * @param integer $parent_id
115
     * @param array $user_accessible_folders
116
     * @param boolean $user_is_admin
117
     * @param boolean $user_can_create_root_folder
118
     * @return boolean
119
     */
120
    private function isParentFolderAllowed($parent_id, $user_accessible_folders, $user_is_admin, $user_can_create_root_folder)
121
    {
122
        if ($parent_id == 0 && $user_can_create_root_folder == true)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
123
	    return true;
124
125
        if (in_array($parent_id, $user_accessible_folders) === false
126
            && (int) $user_is_admin !== 1
127
        ) {
128
            return false;
129
        }
130
        return true;
131
    }
132
133
    /**
134
     * Check if duplicate folder is allowed
135
     *
136
     * @param string $title
137
     * @return boolean
138
     */
139
    private function checkDuplicateFolderAllowed($title)
140
    {
141
        if (
142
            isset($this->settings['duplicate_folder']) === true
143
            && (int) $this->settings['duplicate_folder'] === 0
144
        ) {
145
            DB::query(
146
                'SELECT *
147
                FROM ' . prefixTable('nested_tree') . '
148
                WHERE title = %s AND personal_folder = 0',
149
                $title
150
            );
151
            $counter = DB::count();
152
            if ($counter !== 0) {
153
                return false;
154
            }
155
            return true;
156
        }
157
        return true;
158
    }
159
160
    /**
161
     * Get parent folder data
162
     *
163
     * @param integer $parent_id
164
     * @return array
165
     */
166
    private function getParentFolderData($parent_id)
167
    {
168
        //check if parent folder is personal
169
        $data = DB::queryFirstRow(
170
            'SELECT personal_folder, bloquer_creation, bloquer_modification
171
            FROM ' . prefixTable('nested_tree') . '
172
            WHERE id = %i',
173
            $parent_id
174
        );
175
176
        // inherit from parent the specific settings it has
177
        if (DB::count() > 0) {
178
            $parentBloquerCreation = $data['bloquer_creation'];
179
            $parentBloquerModification = $data['bloquer_modification'];
180
        } else {
181
            $parentBloquerCreation = 0;
182
            $parentBloquerModification = 0;
183
        }
184
185
        return [
186
            'isPersonal' => null !== $data['personal_folder'] ? $data['personal_folder'] : 0,
187
            'parentBloquerCreation' => $parentBloquerCreation,
188
            'parentBloquerModification' => $parentBloquerModification,
189
        ];
190
    }
191
192
    /**
193
     * Check complexity level
194
     *
195
     * @param array $data
196
     * @param integer $complexity
197
     * @param integer $parent_id
198
     * @return array|boolean
199
     */
200
    private function checkComplexityLevel(
201
        $data,
202
        $complexity,
203
        $parent_id
204
    )
205
    {
206
        if ((int) $data['isPersonal'] === 0) {    
207
            // get complexity level for this folder
208
            $data = DB::queryFirstRow(
209
                'SELECT valeur
210
                FROM ' . prefixTable('misc') . '
211
                WHERE intitule = %i AND type = %s',
212
                $parent_id,
213
                'complex'
214
            );
215
            if (isset($data['valeur']) === true && intval($complexity) < intval($data['valeur'])) {
216
                return [
217
                    'error' => true,
218
                ];
219
            }
220
        }
221
222
        return [
223
            'parent_complexity' => isset($data['valeur']) === true ? $data['valeur'] : 0,
224
        ];
225
    }
226
227
    /**
228
     * Creates a new folder in the system, applying necessary settings, permissions, 
229
     * cache updates, and user role management.
230
     *
231
     * @param array $params - Parameters for folder creation (e.g., title, icon, etc.)
232
     * @param array $parentFolderData - Parent folder data (e.g., ID, permissions)
233
     * @return array - Returns an array indicating success or failure
234
     */
235
    private function createFolder($params, $parentFolderData, $options)
236
    {
237
        // Decompose parameters
238
        $title = $params['title'] ?? '';
239
        $parent_id = $params['parent_id'] ?? 0;
240
        $isPersonal = $params['personal_folder'] ?? 0;
241
        $complexity = $params['complexity'] ?? 0;
242
        $access_rights = $params['access_rights'] ?? '';
243
        $user_is_admin = $params['user_is_admin'] ?? 0;
244
        $user_is_manager = $params['user_is_manager'] ?? 0;
245
        $user_can_create_root_folder = $params['user_can_create_root_folder'] ?? 0;
246
        $user_can_manage_all_users = $params['user_can_manage_all_users'] ?? 0;
247
        $user_id = $params['user_id'] ?? 0;
248
        $user_roles = $params['user_roles'] ?? '';    
249
250
        if ($this->canCreateFolder($isPersonal, $user_is_admin, $user_is_manager, $user_can_manage_all_users, $user_can_create_root_folder)) {
251
            $newId = $this->insertFolder($params, $parentFolderData);
252
            $this->addComplexity($newId, $complexity);
253
            if (isset($options['setFolderCategories']) && $options['setFolderCategories'] === true) {
254
                $this->setFolderCategories($newId);
255
            }
256
            $this->updateTimestamp();
257
            if (isset($options['rebuildFolderTree']) && $options['rebuildFolderTree'] === true) {
258
                $this->rebuildFolderTree($user_is_admin, $title, $parent_id, $isPersonal, $user_id, $newId);
259
            }
260
            if (isset($options['manageFolderPermissions']) && $options['manageFolderPermissions'] === true) {
261
                $this->manageFolderPermissions($parent_id, $newId, $user_roles, $access_rights, $user_is_admin);
262
            }
263
            if (isset($options['copyCustomFieldsCategories']) && $options['copyCustomFieldsCategories'] === true) {
264
                $this->copyCustomFieldsCategories($parent_id, $newId);
265
            }
266
            if (isset($options['refreshCacheForUsersWithSimilarRoles']) && $options['refreshCacheForUsersWithSimilarRoles'] === true) {
267
                $this->refreshCacheForUsersWithSimilarRoles($user_roles);
268
            }
269
270
            return ['error' => false, 'newId' => $newId];
271
        } else {
272
            return ['error' => true, 'newId' => null];
273
        }
274
    }
275
276
    /**
277
     * Checks if the user has the permissions to create a folder.
278
     */
279
    private function canCreateFolder($isPersonal, $user_is_admin, $user_is_manager, $user_can_manage_all_users, $user_can_create_root_folder)
280
    {
281
        return (int)$isPersonal === 1 ||
282
            (int)$user_is_admin === 1 ||
283
            ((int)$user_is_manager === 1 || (int)$user_can_manage_all_users === 1) ||
284
            ($this->settings['enable_user_can_create_folders'] ?? false) ||
285
            ((int)$user_can_create_root_folder === 1);
286
    }
287
288
    /**
289
     * Inserts a new folder into the database and returns the new folder ID.
290
     */
291
    private function insertFolder($params, $parentFolderData)
292
    {
293
        DB::insert(prefixTable('nested_tree'), [
294
            'parent_id' => $params['parent_id'],
295
            'title' => $params['title'],
296
            'personal_folder' => $params['personal_folder'] ?? 0,
297
            'renewal_period' => $params['duration'] ?? 0,
298
            'bloquer_creation' => $params['create_auth_without'] ?? $parentFolderData['parentBloquerCreation'],
299
            'bloquer_modification' => $params['edit_auth_without'] ?? $parentFolderData['parentBloquerModification'],
300
            'fa_icon' => empty($params['icon']) ? TP_DEFAULT_ICON : $params['icon'],
301
            'fa_icon_selected' => empty($params['icon_selected']) ? TP_DEFAULT_ICON_SELECTED : $params['icon_selected'],
302
            'categories' => '',
303
        ]);
304
305
        return DB::insertId();
306
    }
307
308
    /**
309
     * Adds complexity settings for the newly created folder.
310
     */
311
    private function addComplexity($folderId, $complexity)
312
    {
313
        DB::insert(prefixTable('misc'), [
314
            'type' => 'complex',
315
            'intitule' => $folderId,
316
            'valeur' => $complexity,
317
            'created_at' => time(),
318
        ]);
319
    }
320
321
    /**
322
     * Ensures that folder categories are set.
323
     */
324
    private function setFolderCategories($folderId)
325
    {
326
        handleFoldersCategories([$folderId]);
327
    }
328
329
    /**
330
     * Updates the last folder change timestamp.
331
     */
332
    private function updateTimestamp()
333
    {
334
        DB::update(prefixTable('misc'), [
335
            'valeur' => time(),
336
            'updated_at' => time(),
337
        ], 'type = %s AND intitule = %s', 'timestamp', 'last_folder_change');
338
    }
339
340
    /**
341
     * Rebuilds the folder tree and updates the cache for non-admin users.
342
     */
343
    private function rebuildFolderTree($user_is_admin, $title, $parent_id, $isPersonal, $user_id, $newId)
344
    {
345
        $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
346
        $tree->rebuild();
347
        
348
        // Update session visible flolders
349
        $sess_key = $isPersonal ? 'user-personal_folders' : 'user-accessible_folders';
350
        SessionManager::addRemoveFromSessionArray($sess_key, [$newId], 'add');
351
352
        if ($user_is_admin === 0) {
353
            $this->updateUserFolderCache($tree, $title, $parent_id, $isPersonal, $user_id, $newId);
354
        }
355
    }
356
357
    /**
358
     * Updates the user folder cache for non-admin users.
359
     */
360
    private function updateUserFolderCache($tree, $title, $parent_id, $isPersonal, $user_id, $newId)
361
    {
362
        $path = '';
363
        $tree_path = $tree->getPath(0, false);
364
        foreach ($tree_path as $fld) {
365
            $path .= empty($path) ? $fld->title : '/' . $fld->title;
366
        }
367
368
        $new_json = [
369
            "path" => $path,
370
            "id" => $newId,
371
            "level" => count($tree_path),
372
            "title" => $title,
373
            "disabled" => 0,
374
            "parent_id" => $parent_id,
375
            "perso" => $isPersonal,
376
            "is_visible_active" => 0,
377
        ];
378
379
        $cache_tree = DB::queryFirstRow(
380
            'SELECT increment_id, folders, visible_folders 
381
            FROM ' . prefixTable('cache_tree') . ' 
382
            WHERE user_id = %i', 
383
            (int) $user_id
384
        );
385
386
        if (empty($cache_tree)) {
387
            DB::insert(
388
                prefixTable('cache_tree'), 
389
                [
390
                    'user_id' => $user_id,
391
                    'folders' => json_encode([$newId]),
392
                    'visible_folders' => json_encode($new_json),
393
                    'timestamp' => time(),
394
                    'data' => '[{}]',
395
                ]
396
            );
397
        } else {
398
            $folders = json_decode($cache_tree['folders'] ?? '[]', true);
399
            $visible_folders = json_decode($cache_tree['visible_folders'] ?? '[]', true);
400
            $folders[] = $newId;
401
            $visible_folders[] = $new_json;
402
403
            DB::update(
404
                prefixTable('cache_tree'), 
405
                [
406
                    'folders' => json_encode($folders),
407
                    'visible_folders' => json_encode($visible_folders),
408
                    'timestamp' => time(),
409
                ],
410
                'increment_id = %i', 
411
                (int) $cache_tree['increment_id']
412
            );
413
        }
414
    }
415
416
    /**
417
     * Manages folder permissions based on user roles or parent folder settings.
418
     */
419
    private function manageFolderPermissions($parent_id, $newId, $user_roles, $access_rights, $user_is_admin)
420
    {
421
        if ($parent_id !== 0 && $this->settings['subfolder_rights_as_parent'] ?? false) {
422
            $rows = DB::query('SELECT role_id, type FROM ' . prefixTable('roles_values') . ' WHERE folder_id = %i', $parent_id);
423
            foreach ($rows as $record) {
424
                DB::insert(prefixTable('roles_values'), [
425
                    'role_id' => $record['role_id'],
426
                    'folder_id' => $newId,
427
                    'type' => $record['type'],
428
                ]);
429
            }
430
        } elseif ((int)$user_is_admin !== 1) {
431
            foreach (array_unique(explode(';', $user_roles)) as $role) {
432
                if (!empty($role)) {
433
                    DB::insert(prefixTable('roles_values'), [
434
                        'role_id' => $role,
435
                        'folder_id' => $newId,
436
                        'type' => $access_rights,
437
                    ]);
438
                }
439
            }
440
        }
441
    }
442
443
    /**
444
     * Copies custom field categories from the parent folder to the newly created folder.
445
     */
446
    private function copyCustomFieldsCategories($parent_id, $newId)
447
    {
448
        $rows = DB::query('SELECT id_category FROM ' . prefixTable('categories_folders') . ' WHERE id_folder = %i', $parent_id);
449
        foreach ($rows as $record) {
450
            DB::insert(prefixTable('categories_folders'), [
451
                'id_category' => $record['id_category'],
452
                'id_folder' => $newId,
453
            ]);
454
        }
455
    }
456
457
    /**
458
     * Refresh the cache for users with similar roles to the current user.
459
     */
460
    private function refreshCacheForUsersWithSimilarRoles($user_roles)
461
    {
462
        $usersWithSimilarRoles = getUsersWithRoles(explode(";", $user_roles));
463
        foreach ($usersWithSimilarRoles as $user) {
464
465
            // Arguments field
466
            $arguments = json_encode([
467
                'user_id' => (int) $user,
468
            ], JSON_HEX_QUOT | JSON_HEX_TAG);
469
470
            // Search for existing job
471
            $count = DB::queryFirstRow(
472
                'SELECT COUNT(*) AS count
473
                FROM ' . prefixTable('background_tasks') . '
474
                WHERE is_in_progress = %i AND process_type = %s AND arguments = %s',
475
                0,
476
                'user_build_cache_tree',
477
                $arguments
478
            )['count'];
479
480
            // Don't insert duplicates
481
            if ($count > 0)
482
                continue;
483
484
            // Insert new background task
485
            DB::insert(
486
                prefixTable('background_tasks'),
487
                array(
488
                    'created_at' => time(),
489
                    'process_type' => 'user_build_cache_tree',
490
                    'arguments' => $arguments,
491
                    'updated_at' => null,
492
                    'finished_at' => null,
493
                    'output' => null,
494
                )
495
            );
496
        }
497
    }
498
499
    /**
500
     * Returns an error response.
501
     */
502
    private function errorResponse($message, $newIdSuffix = "")
503
    {
504
        return [
505
            'error' => true,
506
            'message' => $message,
507
            'newId' => '' . $newIdSuffix,
508
        ];
509
    }
510
}
511