Issues (32)

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.

api/Model/AuthModel.php (1 issue)

1
<?php
2
/**
3
 * Teampass - a collaborative passwords manager.
4
 * ---
5
 * This file is part of the TeamPass project.
6
 * 
7
 * TeamPass is free software: you can redistribute it and/or modify it
8
 * under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, version 3 of the License.
10
 * 
11
 * TeamPass is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 * 
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 * 
19
 * Certain components of this file may be under different licenses. For
20
 * details, see the `licenses` directory or individual file headers.
21
 * ---
22
 * @version    API
23
 *
24
 * @file      AuthModel.php
25
 * @author    Nils Laumaillé ([email protected])
26
 * @copyright 2009-2025 Teampass.net
27
 * @license   GPL-3.0
28
 * @see       https://www.teampass.net
29
 */
30
31
use TeampassClasses\PasswordManager\PasswordManager;
32
use TeampassClasses\NestedTree\NestedTree;
33
use TeampassClasses\ConfigManager\ConfigManager;
34
use Firebase\JWT\JWT;
35
use Firebase\JWT\Key;
36
37
class AuthModel
38
{
39
40
41
    /**
42
     * Is the user allowed
43
     *
44
     * @param string $login
45
     * @param string $password
46
     * @param string $apikey
47
     * @return array
48
     */
49
    public function getUserAuth(string $login, string $password, string $apikey): array
50
    {
51
        // Sanitize
52
        // IMPORTANT: Password should NOT be escaped/sanitized - treat as opaque binary data
53
        // Only trim whitespace which is safe and expected (fix 3.1.5.10)
54
        include_once API_ROOT_PATH . '/../sources/main.functions.php';
55
        $inputData = dataSanitizer(
56
            [
57
                'login' => isset($login) === true ? $login : '',
58
                'password' => isset($password) === true ? $password : '',
59
                'apikey' => isset($apikey) === true ? $apikey : '',
60
            ],
61
            [
62
                'login' => 'trim|escape|strip_tags',
63
                'password' => 'trim', // Only trim, NO escape/sanitization
64
                'apikey' => 'trim|escape|strip_tags',
65
            ]
66
        );
67
        
68
        // Check apikey and credentials
69
        if (empty($inputData['login']) === true || empty($inputData['apikey']) === true || empty($inputData['password']) === true) {
70
            // case where it is a generic key
71
            // Not allowed to use this API
72
73
            return ["error" => "Login failed.", "info" => "User password is requested"];
74
        } else {
75
            // case where it is a user api key
76
            // Check if user exists
77
            $userInfo = getUserCompleteData($inputData['login']);
78
79
            if ($userInfo === null) {
80
                return ["error" => "Login failed.", "info" => "Credentials not valid"];
81
            }
82
83
            // Check if user is enabled
84
            if ((int) $userInfo['api_enabled'] === 0) {
85
                return ["error" => "Login failed.", "info" => "User not allowed to use API"];
86
            }
87
            
88
            // Check password
89
            $passwordManager = new PasswordManager();
90
            if ($passwordManager->verifyPassword($userInfo['pw'], $inputData['password']) === true) {
91
                // Correct credentials
92
                // get user keys
93
                $privateKeyClear = decryptPrivateKey($inputData['password'], (string) $userInfo['private_key']);
94
95
                // check API key
96
                if ($inputData['apikey'] !== base64_decode(decryptUserObjectKey($userInfo['api_key'], $privateKeyClear))) {
97
                    return ["error" => "Login failed.", "info" => "API Key not valid"];
98
                }
99
100
                // Update user's key_tempo
101
                $keyTempo = bin2hex(random_bytes(16));
102
                /*DB::update(
103
                    prefixTable('users'),
104
                    [
105
                        'key_tempo' => $keyTempo,
106
                    ],
107
                    'id = %i',
108
                    $userInfo['id']
109
                );*/
110
111
                // Generate a unique session key for this API session (256 bits / 32 bytes)
112
                // This key will be stored in the JWT and used to decrypt the private key
113
                $sessionKey = random_bytes(32);
114
                $sessionKeySalt = bin2hex(random_bytes(16));
115
116
                // Encrypt the decrypted private key with the session key
117
                // This allows us to store it securely in the database without exposing it
118
                require_once API_ROOT_PATH . '/inc/encryption_utils.php';
119
                $encryptedPrivateKey = encrypt_with_session_key($privateKeyClear, $sessionKey);
120
121
                if ($encryptedPrivateKey === false) {
122
                    return ["error" => "Login failed.", "info" => "Failed to encrypt private key"];
123
                }
124
125
                // Store the ENCRYPTED private key in the API table
126
                // Even if the database is compromised, the key cannot be used without the session_key from the JWT
127
                DB::update(
128
                    prefixTable('api'),
129
                    [
130
                        'encrypted_private_key' => $encryptedPrivateKey,
131
                        'session_key_salt' => $sessionKeySalt,
132
                        'session_key' => $keyTempo,
133
                        'timestamp' => time(),
134
                    ],
135
                    'user_id = %i',
136
                    $userInfo['id']
137
                );
138
139
                // get user folders list
140
                $ret = $this->buildUserFoldersList($userInfo);
141
142
                // Load config
143
                $configManager = new ConfigManager();
144
                $SETTINGS = $configManager->getAllSettings();
145
146
                // Log user
147
                logEvents($SETTINGS, 'api', 'user_connection', (string) $userInfo['id'], stripslashes($userInfo['login']));
148
149
                // create JWT with session key
150
                return $this->createUserJWT(
151
                    (int) $userInfo['id'],
152
                    (string) $inputData['login'],
153
                    (int) $userInfo['personal_folder'],
154
                    (string) implode(",", $ret['folders']),
155
                    (string) implode(",", $ret['items']),
156
                    (string) $keyTempo,
157
                    (string) base64_encode($sessionKey), // Session key for decrypting private key
158
                    (int) $userInfo['admin'],
159
                    (int) $userInfo['gestionnaire'],
160
                    (int) $userInfo['can_create_root_folder'],
161
                    (int) $userInfo['can_manage_all_users'],
162
                    (string) $userInfo['fonction_id'],
163
                    (string) $userInfo['api_allowed_folders'],
164
                    (int) $userInfo['api_allowed_to_create'],
165
                    (int) $userInfo['api_allowed_to_read'],
166
                    (int) $userInfo['api_allowed_to_update'],
167
                    (int) $userInfo['api_allowed_to_delete'],
168
                    (int) $SETTINGS['api_token_duration'] ?? 60,
0 ignored issues
show
The call to AuthModel::createUserJWT() has too many arguments starting with (int)$SETTINGS['api_token_duration'] ?? 60. ( Ignorable by Annotation )

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

168
                return $this->/** @scrutinizer ignore-call */ createUserJWT(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
169
                );
170
            } else {
171
                return ["error" => "Login failed.", "info" => "Credentials not valid"];
172
            }
173
        }
174
    }
175
    //end getUserAuth
176
177
    /**
178
     * Create a JWT
179
     *
180
     * Note: User encryption keys (public_key, private_key) are no longer stored in the JWT
181
     * to reduce token size. Instead, the private key is encrypted with a session key and
182
     * stored in the database. The session key is stored in the JWT (~44 bytes) which is
183
     * used to decrypt the private key when needed.
184
     *
185
     * Security approach (defense in depth):
186
     * - Encrypted private key in DB is useless without session_key
187
     * - session_key in JWT is useless without encrypted private key from DB
188
     * - Both are required together to access the decrypted private key
189
     *
190
     * @param integer $id
191
     * @param string $login
192
     * @param integer $pf_enabled
193
     * @param string $folders
194
     * @param string $items
195
     * @param string $keyTempo
196
     * @param string $sessionKey Session key (base64) for decrypting private key from DB
197
     * @param integer $admin
198
     * @param integer $manager
199
     * @param integer $can_create_root_folder
200
     * @param integer $can_manage_all_users
201
     * @param string $roles
202
     * @param string $allowed_folders
203
     * @param integer $allowed_to_create
204
     * @param integer $allowed_to_read
205
     * @param integer $allowed_to_update
206
     * @param integer $allowed_to_delete
207
     * @return array
208
     */
209
    private function createUserJWT(
210
        int $id,
211
        string $login,
212
        int $pf_enabled,
213
        string $folders,
214
        string $items,
215
        string $keyTempo,
216
        string $sessionKey,
217
        int $admin,
218
        int $manager,
219
        int $can_create_root_folder,
220
        int $can_manage_all_users,
221
        string $roles,
222
        string $allowed_folders,
223
        int $allowed_to_create,
224
        int $allowed_to_read,
225
        int $allowed_to_update,
226
        int $allowed_to_delete,
227
    ): array
228
    {
229
        // Load config
230
        $configManager = new ConfigManager();
231
        $SETTINGS = $configManager->getAllSettings();
232
233
		$payload = [
234
            'username' => $login,
235
            'id' => $id,
236
            'exp' => (time() + $SETTINGS['api_token_duration'] + 600),
237
            'pf_enabled' => $pf_enabled,
238
            'folders_list' => $folders,
239
            'restricted_items_list' => $items,
240
            'key_tempo' => $keyTempo,
241
            'session_key' => $sessionKey, // Session key for decrypting private key from database
242
            'is_admin' => $admin,
243
            'is_manager' => $manager,
244
            'user_can_create_root_folder' => $can_create_root_folder,
245
            'user_can_manage_all_users' => $can_manage_all_users,
246
            'roles' => $roles,
247
            'allowed_folders' => $allowed_folders,
248
            'allowed_to_create' => $allowed_to_create,
249
            'allowed_to_read' => $allowed_to_read,
250
            'allowed_to_update' => $allowed_to_update,
251
            'allowed_to_delete' => $allowed_to_delete,
252
        ];
253
254
        return ['token' => JWT::encode($payload, DB_PASSWD, 'HS256')];
255
    }
256
257
    //end createUserJWT
258
259
260
    /**
261
     * Permit to build the list of folders the user can access
262
     *
263
     * @param array $userInfo
264
     * @return array
265
     */
266
    private function buildUserFoldersList(array $userInfo): array
267
    {
268
        //Build tree
269
        $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
270
        
271
        // Start by adding the manually added folders
272
        $allowedFolders = array_map('intval', explode(";", $userInfo['groupes_visibles']));
273
        $readOnlyFolders = [];
274
        $allowedFoldersByRoles = [];
275
        $restrictedFoldersForItems = [];
276
        $foldersLimited = [];
277
        $foldersLimitedFull = [];
278
        $restrictedItems = [];
279
        $personalFolders = [];
280
281
        $userFunctionId = explode(";", $userInfo['fonction_id']);
282
283
        // Get folders from the roles
284
        if (count($userFunctionId) > 0) {
285
            $rows = DB::query(
286
                'SELECT * 
287
                FROM ' . prefixTable('roles_values') . '
288
                WHERE role_id IN %li  AND type IN ("W", "ND", "NE", "NDNE", "R")',
289
                $userFunctionId
290
            );
291
            foreach ($rows as $record) {
292
                if ($record['type'] === 'R') {
293
                    array_push($readOnlyFolders, $record['folder_id']);
294
                } elseif (in_array($record['folder_id'], $allowedFolders) === false) {
295
                    array_push($allowedFoldersByRoles, $record['folder_id']);
296
                }
297
            }
298
            $allowedFoldersByRoles = array_unique($allowedFoldersByRoles);
299
            $readOnlyFolders = array_unique($readOnlyFolders);
300
            // Clean arrays
301
            foreach ($allowedFoldersByRoles as $value) {
302
                $key = array_search($value, $readOnlyFolders);
303
                if ($key !== false) {
304
                    unset($readOnlyFolders[$key]);
305
                }
306
            }
307
        }
308
        
309
        // Does this user is allowed to see other items
310
        $inc = 0;
311
        $rows = DB::query(
312
            'SELECT id, id_tree 
313
            FROM ' . prefixTable('items') . '
314
            WHERE restricted_to LIKE %s'.
315
            (count($userFunctionId) > 0 ? ' AND id_tree NOT IN %li' : ''),
316
            $userInfo['id'],
317
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
318
        );
319
        foreach ($rows as $record) {
320
            // Exclude restriction on item if folder is fully accessible
321
            $restrictedFoldersForItems[$inc] = $record['id_tree'];
322
            ++$inc;
323
        }
324
325
        // Check for the users roles if some specific rights exist on items
326
        $rows = DB::query(
327
            'SELECT i.id_tree, r.item_id
328
            FROM ' . prefixTable('items') . ' AS i
329
            INNER JOIN ' . prefixTable('restriction_to_roles') . ' AS r ON (r.item_id=i.id)
330
            WHERE '.(count($userFunctionId) > 0 ? ' id_tree NOT IN %li AND ' : '').' i.id_tree != ""
331
            ORDER BY i.id_tree ASC',
332
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
333
        );
334
        foreach ($rows as $record) {
335
            $foldersLimited[$record['id_tree']][$inc] = $record['item_id'];
336
            //array_push($foldersLimitedFull, $record['item_id']);
337
            array_push($restrictedItems, $record['item_id']);
338
            array_push($foldersLimitedFull, $record['id_tree']);
339
            ++$inc;
340
        }
341
342
        // Add all personal folders
343
        $rows = DB::queryFirstRow(
344
            'SELECT id 
345
            FROM ' . prefixTable('nested_tree') . '
346
            WHERE title = %i AND personal_folder = 1'.
347
            (count($userFunctionId) > 0 ? ' AND id NOT IN %li' : ''),
348
            $userInfo['id'],
349
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
350
        );
351
        if (empty($rows['id']) === false) {
352
            array_push($personalFolders, $rows['id']);
353
            // get all descendants
354
            $ids = $tree->getDescendants($rows['id'], false, false, true);
355
            foreach ($ids as $id) {
356
                array_push($personalFolders, $id);
357
            }
358
        }
359
360
        // All folders visibles
361
        return [
362
            'folders' => array_unique(
363
            array_filter(
364
                array_merge(
365
                    $allowedFolders,
366
                    $foldersLimitedFull,
367
                    $allowedFoldersByRoles,
368
                    $restrictedFoldersForItems,
369
                    $readOnlyFolders,
370
                    $personalFolders
371
                )
372
                )
373
            ),
374
            'items' => array_unique($restrictedItems),
375
        ];
376
    }
377
    //end buildUserFoldersList
378
}