Issues (29)

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)

Labels
Severity
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
error_log(print_r($inputData, true));
0 ignored issues
show
It seems like print_r($inputData, true) can also be of type true; however, parameter $message of error_log() does only seem to accept string, 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

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