Issues (43)

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/Controller/Api/ItemController.php (2 issues)

1
<?php
2
/**
3
 * Teampass - a collaborative passwords manager.
4
 * ---
5
 * This library is distributed in the hope that it will be useful,
6
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
7
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8
 * ---
9
 *
10
 * @project   Teampass
11
 * @version    API
12
 *
13
 * @file      ItemControler.php
14
 * ---
15
 *
16
 * @author    Nils Laumaillé ([email protected])
17
 *
18
 * @copyright 2009-2026 Teampass.net
19
 *
20
 * @license   https://spdx.org/licenses/GPL-3.0-only.html#licenseText GPL-3.0
21
 * ---
22
 *
23
 * @see       https://www.teampass.net
24
 */
25
26
use Symfony\Component\HttpFoundation\Request AS symfonyRequest;
27
28
class ItemController extends BaseController
29
{
30
31
    /**
32
     * Get user's private key from database
33
     *
34
     * Retrieves the user's private key by decrypting it from the database.
35
     * The private key is stored ENCRYPTED in the database and is decrypted
36
     * using the session_key from the JWT token.
37
     *
38
     * @param array $userData User data from JWT token (must include id, key_tempo, and session_key)
39
     * @return string|null Decrypted private key or null if not found/invalid session
40
     */
41
    private function getUserPrivateKey(array $userData): ?string
42
    {
43
        include_once API_ROOT_PATH . '/inc/jwt_utils.php';
44
45
        // Verify session_key exists in JWT payload
46
        if (!isset($userData['session_key']) || empty($userData['session_key'])) {
47
            error_log('getUserPrivateKey: Missing session_key in JWT token for user ID ' . $userData['id']);
48
            return null;
49
        }
50
51
        $userKeys = get_user_keys(
52
            (int) $userData['id'],
53
            (string) $userData['key_tempo'],
54
            (string) $userData['session_key'] // Session key from JWT for decryption
55
        );
56
57
        if ($userKeys === null) {
58
            return null;
59
        }
60
61
        return $userKeys['private_key'];
62
    }
63
64
65
    /**
66
     * Manage case inFolder - get items inside an array of folders
67
     *
68
     * @param array $userData
69
     */
70
    public function inFoldersAction(array $userData): void
71
    {
72
        $request = symfonyRequest::createFromGlobals();
73
        $requestMethod = $request->getMethod();
74
        $strErrorDesc = $responseData = $strErrorHeader = '';
75
76
        // get parameters
77
        $arrQueryStringParams = $this->getQueryStringParams();
78
79
        if (strtoupper($requestMethod) === 'GET') {
80
            // define WHERE clause
81
            $sqlExtra = 'WHERE i.deleted_at IS NULL';
82
            if (empty($userData['folders_list']) === false) {
83
                $userData['folders_list'] = explode(',', $userData['folders_list']);
84
            } else {
85
                $userData['folders_list'] = [];
86
            }
87
88
            // SQL where clause with folders list
89
            if (isset($arrQueryStringParams['folders']) === true) {
90
                // convert the folders to an array
91
                $arrQueryStringParams['folders'] = explode(',', str_replace( array('[',']') , ''  , $arrQueryStringParams['folders']));
92
93
                // ensure to only use the intersection
94
                $foldersList = implode(',', array_intersect($arrQueryStringParams['folders'], $userData['folders_list']));
95
96
                // build sql where clause
97
                if (!empty($foldersList)) {
98
                    // build sql where clause
99
                    $sqlExtra .= ' AND i.id_tree IN ('.$foldersList.')';
100
                } else {
101
                    // Send error
102
                    $this->sendOutput(
103
                        json_encode(['error' => 'Folders are mandatory']),
104
                        ['Content-Type: application/json', 'HTTP/1.1 401 Expected parameters not provided']
105
                    );
106
                }
107
            } else {
108
                // Send error
109
                $this->sendOutput(
110
                    json_encode(['error' => 'Folders are mandatory']),
111
                    ['Content-Type: application/json', 'HTTP/1.1 401 Expected parameters not provided']
112
                );
113
            }
114
115
            // SQL LIMIT
116
            $intLimit = 0;
117
            if (isset($arrQueryStringParams['limit']) === true) {
118
                $intLimit = $arrQueryStringParams['limit'];
119
            }
120
121
            // send query
122
            try {
123
                $itemModel = new ItemModel();
124
125
                // Get user's private key from database
126
                $userPrivateKey = $this->getUserPrivateKey($userData);
127
                if ($userPrivateKey === null) {
128
                    $strErrorDesc = 'Invalid session or user keys not found';
129
                    $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
130
                } else {
131
                    $arrItems = $itemModel->getItems($sqlExtra, $intLimit, $userPrivateKey, $userData['id']);
132
                    if (!empty($arrItems)) {
133
                        $responseData = json_encode($arrItems);
134
                    } else {
135
                        $strErrorDesc = 'No content for this label';
136
                        $strErrorHeader = 'HTTP/1.1 204 No Content';
137
                    }
138
                }
139
            } catch (Error $e) {
140
                $strErrorDesc = $e->getMessage().'. Something went wrong! Please contact support.4';
141
                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
142
            }
143
        } else {
144
            $strErrorDesc = 'Method not supported';
145
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
146
        }
147
148
        // send output
149
        if (empty($strErrorDesc) === true) {
150
            $this->sendOutput(
151
                $responseData,
152
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
153
            );
154
        } else {
155
            $this->sendOutput(
156
                json_encode(['error' => $strErrorDesc]), 
157
                ['Content-Type: application/json', $strErrorHeader]
158
            );
159
        }
160
    }
161
    //end InFoldersAction()
162
163
    private function checkNewItemData(array $arrQueryStringParams, array $userData): array
164
    {
165
        if (isset($arrQueryStringParams['label']) === true
166
            && isset($arrQueryStringParams['folder_id']) === true
167
            && isset($arrQueryStringParams['password']) === true
168
            && isset($arrQueryStringParams['login']) === true
169
            && isset($arrQueryStringParams['email']) === true
170
            && isset($arrQueryStringParams['url']) === true
171
            && isset($arrQueryStringParams['tags']) === true
172
            && isset($arrQueryStringParams['anyone_can_modify']) === true
173
        ) {
174
            //
175
            if (in_array($arrQueryStringParams['folder_id'], $userData['folders_list']) === false && $userData['user_can_create_root_folder'] === 0) {
176
                return [
177
                    'error' => true,
178
                    'strErrorDesc' => 'User is not allowed in this folder',
179
                    'strErrorHeader' => 'HTTP/1.1 401 Unauthorized',
180
                ];
181
            } else if (empty($arrQueryStringParams['label']) === true) {
182
                return [
183
                    'error' => true,
184
                    'strErrorDesc' => 'Label is mandatory',
185
                    'strErrorHeader' => 'HTTP/1.1 401 Expected parameters not provided',
186
                ];
187
            } else {
188
                return [
189
                    'error' => false,
190
                ];
191
            }
192
        }
193
194
        return [
195
            'error' => true,
196
            'strErrorDesc' => 'All fields have to be provided even if empty (refer to documentation).',
197
            'strErrorHeader' => 'HTTP/1.1 401 Expected parameters not provided',
198
        ];
199
    }
200
201
    /**
202
     * Manage case Add
203
     *
204
     * @param array $userData User data from JWT token
205
     * @return void
206
     */
207
    public function createAction(array $userData)
208
    {
209
        $request = symfonyRequest::createFromGlobals();
210
        $requestMethod = $request->getMethod();
211
        $strErrorDesc = $strErrorHeader = $responseData = '';
212
213
        if (strtoupper($requestMethod) === 'POST') {
214
            // Is user allowed to create a folder
215
            // We check if allowed_to_create
216
            if ((int) $userData['allowed_to_create'] !== 1) {
217
                $strErrorDesc = 'User is not allowed to create an item';
218
                $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
219
            } else {
220
                if (empty($userData['folders_list']) === false) {
221
                    $userData['folders_list'] = explode(',', $userData['folders_list']);
222
                } else {
223
                    $userData['folders_list'] = [];
224
                }
225
226
                // get parameters
227
                $arrQueryStringParams = $this->getQueryStringParams();
228
229
                // Check that the parameters are indeed an array before using them
230
                if (is_array($arrQueryStringParams)) {
231
                    // check parameters
232
                    $arrCheck = $this->checkNewItemData($arrQueryStringParams, $userData);
233
234
                    if ($arrCheck['error'] === true) {
235
                        $strErrorDesc = $arrCheck['strErrorDesc'];
236
                        $strErrorHeader = $arrCheck['strErrorHeader'];
237
                    } else {
238
                        // Prepare array of item parameters
239
                        $arrItemParams = [
240
                            'folder_id' => (int) $arrQueryStringParams['folder_id'],
241
                            'label' => (string) $arrQueryStringParams['label'],
242
                            'password' => (string) $arrQueryStringParams['password'],
243
                            'description' => (string) ($arrQueryStringParams['description'] ?? ''),
244
                            'login' => (string) $arrQueryStringParams['login'],
245
                            'email' => (string) ($arrQueryStringParams['email'] ?? ''),
246
                            'url' => (string) ($arrQueryStringParams['url'] ?? ''),
247
                            'tags' => (string) ($arrQueryStringParams['tags'] ?? ''),
248
                            'anyone_can_modify' => (int) $arrQueryStringParams['anyone_can_modify'] ?? 0,
249
                            'icon' => (string) $arrQueryStringParams['icon'] ?? '',
250
                            'id' => (int) $userData['id'],
251
                            'username' => (string) $userData['username'],
252
                            'totp' => (string) ($userData['totp'] ?? ''),
253
                        ];
254
255
                        // launch
256
                        $itemModel = new ItemModel();
257
                        $ret = $itemModel->addItem(
258
                            $arrItemParams
259
                        );
260
                        $responseData = json_encode($ret);
261
                    }
262
                
263
                } else {
264
                    // Gérer le cas où les paramètres ne sont pas un tableau
265
                    $strErrorDesc = 'Data not consistent';
266
                    $strErrorHeader = 'Expected array, received ' . gettype($arrQueryStringParams);
267
                }
268
            }
269
        } else {
270
            $strErrorDesc = 'Method not supported';
271
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
272
        }
273
274
        // send output
275
        if (empty($strErrorDesc) === true) {
276
            $this->sendOutput(
277
                $responseData,
278
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
279
            );
280
        } else {
281
            $this->sendOutput(
282
                json_encode(['error' => $strErrorDesc]),
283
                ['Content-Type: application/json', $strErrorHeader]
284
            );
285
        }
286
    }
287
    //end addAction()
288
289
290
    /**
291
     * Manage case get - get an item
292
     *
293
     * @param array $userData
294
     */
295
    public function getAction(array $userData): void
296
    {
297
        
298
        $request = symfonyRequest::createFromGlobals();
299
        $requestMethod = $request->getMethod();
300
        $strErrorDesc = '';
301
        $showItem = false;
302
        $sqlExtra = 'WHERE i.deleted_at IS NULL';
303
        $sqlLimit = 0;
304
        $responseData = '';
305
        $strErrorHeader = '';
306
        $sql_constraint = ' AND (i.id_tree IN ('.$userData['folders_list'].')';
307
        if (!empty($userData['restricted_items_list'])) {
308
            $sql_constraint .= 'OR i.id IN ('.$userData['restricted_items_list'].')';
309
        }
310
        $sql_constraint .= ')';
311
312
        // get parameters
313
        $arrQueryStringParams = $this->getQueryStringParams();
314
315
        if (strtoupper($requestMethod) === 'GET') {
316
            // SQL where clause with item id
317
            if (isset($arrQueryStringParams['id']) === true) {
318
                // build sql where clause by ID
319
                $sqlExtra .= ' AND i.id = '.$arrQueryStringParams['id'] . $sql_constraint;
320
                $showItem = true;
321
            } else if (isset($arrQueryStringParams['label']) === true) {
322
                // build sql where clause by LABEL
323
                $sqlExtra .= ' AND i.label '.(isset($arrQueryStringParams['like']) === true && (int) $arrQueryStringParams['like'] === 1 ? ' LIKE "%'.$arrQueryStringParams['label'].'%"' : ' = '.$arrQueryStringParams['label']) . $sql_constraint;
324
                $sqlLimit = isset($arrQueryStringParams['limit']) === true && (int) $arrQueryStringParams['limit'] > 0 ? $arrQueryStringParams['limit'] : 50;   // let's limit to 50 by default
325
            } else if (isset($arrQueryStringParams['description']) === true) {
326
                // build sql where clause by DESCRIPTION
327
                $sqlExtra .= ' AND i.description '.(isset($arrQueryStringParams['like']) === true && (int) $arrQueryStringParams['like'] === 1 ? ' LIKE '.$arrQueryStringParams['description'] : ' = '.$arrQueryStringParams['description']).$sql_constraint;
328
            } else {
329
                // Send error
330
                $this->sendOutput(
331
                    json_encode(['error' => 'Item id, label or description is mandatory']),
332
                    ['Content-Type: application/json', 'HTTP/1.1 401 Expected parameters not provided']
333
                );
334
            }
335
336
            // send query
337
            try {
338
                $itemModel = new ItemModel();
339
340
                // Get user's private key from database
341
                $userPrivateKey = $this->getUserPrivateKey($userData);
342
                if ($userPrivateKey === null) {
343
                    $strErrorDesc = 'Invalid session or user keys not found';
344
                    $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
345
                } else {
346
                    $arrItems = $itemModel->getItems($sqlExtra, $sqlLimit, $userPrivateKey, $userData['id'], $showItem);
347
                    $responseData = json_encode($arrItems);
348
                }
349
            } catch (Error $e) {
350
                $strErrorDesc = $e->getMessage().'. Something went wrong! Please contact support.6';
351
                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
352
            }
353
        } else {
354
            $strErrorDesc = 'Method not supported';
355
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
356
        }
357
358
        // send output
359
        if (empty($strErrorDesc) === true) {
360
            $this->sendOutput(
361
                $responseData,
362
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
363
            );
364
        } else {
365
            $this->sendOutput(
366
                json_encode(['error' => $strErrorDesc]), 
367
                ['Content-Type: application/json', $strErrorHeader]
368
            );
369
        }
370
    }
371
    //end getAction()
372
373
    /**
374
     * Find items by URL
375
     * Searches for items matching a specific URL
376
     *
377
     * @param array $userData User data from JWT token
378
     * @return void
379
     */
380
    public function findByUrlAction(array $userData): void
381
    {
382
        $request = symfonyRequest::createFromGlobals();
383
        $requestMethod = $request->getMethod();
384
        $strErrorDesc = $responseData = $strErrorHeader = '';
385
386
        // Get parameters
387
        $arrQueryStringParams = $this->getQueryStringParams();
388
389
        if (strtoupper($requestMethod) === 'GET') {
390
            // Check if URL parameter is provided
391
            if (isset($arrQueryStringParams['url']) === false || empty($arrQueryStringParams['url']) === true) {
392
                $this->sendOutput(
393
                    json_encode(['error' => 'URL parameter is mandatory']),
394
                    ['Content-Type: application/json', 'HTTP/1.1 400 Bad Request']
395
                );
396
                return;
397
            }
398
399
            // Prepare user's accessible folders
400
            /*if (empty($userData['folders_list']) === false) {
401
                $userData['folders_list'] = explode(',', $userData['folders_list']);
402
            } else {
403
                $userData['folders_list'] = [];
404
            }*/
405
406
            // Build SQL constraint for accessible folders
407
            $sql_constraint = ' AND (i.id_tree IN (' . $userData['folders_list'] . ')';
408
            if (!empty($userData['restricted_items_list'])) {
409
                $sql_constraint .= ' OR i.id IN (' . $userData['restricted_items_list'] . ')';
410
            }
411
            $sql_constraint .= ')';
412
413
            // Decode URL if needed
414
            $searchUrl = urldecode($arrQueryStringParams['url']);
415
416
            try {
417
                // Get user's private key from database
418
                $userPrivateKey = $this->getUserPrivateKey($userData);
419
                if ($userPrivateKey === null) {
420
                    $strErrorDesc = 'Invalid session or user keys not found';
421
                    $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
422
                } else {
423
                    // Query items with the specific URL
424
                    $rows = DB::query(
425
                        "SELECT i.id, i.label, i.login, i.url, i.id_tree, i.favicon_url,
426
                                CASE WHEN o.enabled = 1 THEN 1 ELSE 0 END AS has_otp
427
                        FROM " . prefixTable('items') . " AS i
428
                        LEFT JOIN " . prefixTable('items_otp') . " AS o ON (o.item_id = i.id)
429
                        WHERE i.url LIKE %s" . $sql_constraint . "
430
                            AND i.deleted_at IS NULL
431
                        ORDER BY i.label ASC",
432
                        "%".$searchUrl."%"
433
                    );
434
435
                    $ret = [];
436
                    foreach ($rows as $row) {
437
                        // Get user's sharekey for this item
438
                        $shareKey = DB::queryfirstrow(
0 ignored issues
show
The assignment to $shareKey is dead and can be removed.
Loading history...
439
                            'SELECT share_key
440
                            FROM ' . prefixTable('sharekeys_items') . '
441
                            WHERE user_id = %i AND object_id = %i',
442
                            $userData['id'],
443
                            $row['id']
444
                        );
445
446
                        // Skip if no sharekey found (user doesn't have access)
447
                        if (DB::count() === 0) {
448
                            continue;
449
                        }
450
451
                        // Build response
452
                        array_push(
453
                            $ret,
454
                            [
455
                                'id' => (int) $row['id'],
456
                                'label' => $row['label'],
457
                                'login' => $row['login'],
458
                                'url' => $row['url'],
459
                                'folder_id' => (int) $row['id_tree'],
460
                                'has_otp' => (int) $row['has_otp'],
461
                                'favicon_url' => (string) $row['favicon_url'],
462
                            ]
463
                        );
464
                    }
465
466
                    if (!empty($ret)) {
467
                        $responseData = json_encode($ret);
468
                    } else {
469
                        $strErrorDesc = 'No items found with this URL';
470
                        $strErrorHeader = 'HTTP/1.1 204 No Content';
471
                    }
472
                }
473
            } catch (Error $e) {
474
                $strErrorDesc = $e->getMessage() . '. Something went wrong! Please contact support.';
475
                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
476
            }
477
        } else {
478
            $strErrorDesc = 'Method not supported';
479
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
480
        }
481
482
        // Send output
483
        if (empty($strErrorDesc) === true) {
484
            $this->sendOutput(
485
                $responseData,
486
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
487
            );
488
        } else {
489
            $this->sendOutput(
490
                json_encode(['error' => $strErrorDesc]),
491
                ['Content-Type: application/json', $strErrorHeader]
492
            );
493
        }
494
    }
495
    //end findByUrlAction()
496
497
498
    /**
499
     * Get OTP/TOTP code for an item
500
     * Retrieves the current 6-digit TOTP code for an item with OTP enabled
501
     *
502
     * @param array $userData User data from JWT token
503
     * @return void
504
     */
505
    public function getOtpAction(array $userData): void
506
    {
507
        $request = symfonyRequest::createFromGlobals();
508
        $requestMethod = $request->getMethod();
509
        $strErrorDesc = '';
510
        $responseData = '';
511
        $strErrorHeader = '';
512
513
        // get parameters
514
        $arrQueryStringParams = $this->getQueryStringParams();
515
516
        if (strtoupper($requestMethod) === 'GET') {
517
            // Check if item ID is provided
518
            if (!isset($arrQueryStringParams['id']) || empty($arrQueryStringParams['id'])) {
519
                $this->sendOutput(
520
                    json_encode(['error' => 'Item id is mandatory']),
521
                    ['Content-Type: application/json', 'HTTP/1.1 400 Bad Request']
522
                );
523
                return;
524
            }
525
526
            $itemId = (int) $arrQueryStringParams['id'];
527
528
            try {
529
                // Load config
530
                loadClasses('DB');
531
532
                // Load item basic info to check folder access
533
                $itemInfo = DB::queryFirstRow(
534
                    'SELECT id_tree FROM ' . prefixTable('items') . ' WHERE id = %i',
535
                    $itemId
536
                );
537
538
                if (DB::count() === 0) {
539
                    $strErrorDesc = 'Item not found';
540
                    $strErrorHeader = 'HTTP/1.1 404 Not Found';
541
                } else {
542
                    // Check if user has access to the folder
543
                    $userFolders = !empty($userData['folders_list']) ? explode(',', $userData['folders_list']) : [];
544
                    $hasAccess = in_array((string) $itemInfo['id_tree'], $userFolders, true);
545
546
                    // Also check restricted items if applicable
547
                    if (!$hasAccess && !empty($userData['restricted_items_list'])) {
548
                        $restrictedItems = explode(',', $userData['restricted_items_list']);
549
                        $hasAccess = in_array((string) $itemId, $restrictedItems, true);
550
                    }
551
552
                    if (!$hasAccess) {
553
                        $strErrorDesc = 'Access denied to this item';
554
                        $strErrorHeader = 'HTTP/1.1 403 Forbidden';
555
                    } else {
556
                        // Load OTP data
557
                        $otpData = DB::queryFirstRow(
558
                            'SELECT secret, enabled FROM ' . prefixTable('items_otp') . ' WHERE item_id = %i',
559
                            $itemId
560
                        );
561
562
                        if (DB::count() === 0) {
563
                            $strErrorDesc = 'OTP not configured for this item';
564
                            $strErrorHeader = 'HTTP/1.1 404 Not Found';
565
                        } elseif ((int) $otpData['enabled'] !== 1) {
566
                            $strErrorDesc = 'OTP is not enabled for this item';
567
                            $strErrorHeader = 'HTTP/1.1 403 Forbidden';
568
                        } else {
569
                            // Decrypt the secret
570
                            $decryptedSecret = cryption(
571
                                $otpData['secret'],
572
                                '',
573
                                'decrypt'
574
                            );
575
576
                            if (isset($decryptedSecret['string']) && !empty($decryptedSecret['string'])) {
577
                                // Generate OTP code using OTPHP library
578
                                try {
579
                                    $otp = \OTPHP\TOTP::createFromSecret($decryptedSecret['string']);
580
                                    $otpCode = $otp->now();
581
                                    $otpExpiresIn = $otp->expiresIn();
582
583
                                    $responseData = json_encode([
584
                                        'otp_code' => $otpCode,
585
                                        'expires_in' => $otpExpiresIn,
586
                                        'item_id' => $itemId
587
                                    ]);
588
                                } catch (\RuntimeException $e) {
589
                                    $strErrorDesc = 'Failed to generate OTP code: ' . $e->getMessage();
590
                                    $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
591
                                }
592
                            } else {
593
                                $strErrorDesc = 'Failed to decrypt OTP secret';
594
                                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
595
                            }
596
                        }
597
                    }
598
                }
599
            } catch (\Error $e) {
600
                $strErrorDesc = $e->getMessage() . '. Something went wrong! Please contact support.';
601
                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
602
            }
603
        } else {
604
            $strErrorDesc = 'Method not supported';
605
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
606
        }
607
608
        // send output
609
        if (empty($strErrorDesc) === true) {
610
            $this->sendOutput(
611
                $responseData,
612
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
613
            );
614
        } else {
615
            $this->sendOutput(
616
                json_encode(['error' => $strErrorDesc]),
617
                ['Content-Type: application/json', $strErrorHeader]
618
            );
619
        }
620
    }
621
    //end getOtpAction()
622
623
624
    /**
625
     * Update an existing item
626
     * Updates an item based upon provided parameters and item ID
627
     *
628
     * @param array $userData User data from JWT token
629
     * @return void
630
     */
631
    public function updateAction(array $userData): void
632
    {
633
        $request = symfonyRequest::createFromGlobals();
634
        $requestMethod = $request->getMethod();
635
        $strErrorDesc = $strErrorHeader = $responseData = '';
636
637
        if (strtoupper($requestMethod) === 'PUT' || strtoupper($requestMethod) === 'POST') {
638
            // Check if user is allowed to update items
639
            if ((int) $userData['allowed_to_update'] !== 1) {
640
                $strErrorDesc = 'User is not allowed to update items';
641
                $strErrorHeader = 'HTTP/1.1 403 Forbidden';
642
            } else {
643
                // Prepare user's accessible folders
644
                if (empty($userData['folders_list']) === false) {
645
                    $userData['folders_list'] = explode(',', $userData['folders_list']);
646
                } else {
647
                    $userData['folders_list'] = [];
648
                }
649
650
                // Get parameters
651
                $arrQueryStringParams = $this->getQueryStringParams();
652
653
                // Check that the parameters are indeed an array before using them
654
                if (is_array($arrQueryStringParams)) {
655
                    // Check if item ID is provided
656
                    if (!isset($arrQueryStringParams['id']) || empty($arrQueryStringParams['id'])) {
657
                        $strErrorDesc = 'Item ID is mandatory';
658
                        $strErrorHeader = 'HTTP/1.1 400 Bad Request';
659
                    } else {
660
                        $itemId = (int) $arrQueryStringParams['id'];
661
662
                        try {
663
                            // Load item info to check access rights
664
                            $itemInfo = DB::queryFirstRow(
665
                                'SELECT id, id_tree, label FROM ' . prefixTable('items') . ' WHERE id = %i',
666
                                $itemId
667
                            );
668
669
                            if (DB::count() === 0) {
670
                                $strErrorDesc = 'Item not found';
671
                                $strErrorHeader = 'HTTP/1.1 404 Not Found';
672
                            } else {
673
                                // Check if user has access to the folder
674
                                $hasAccess = in_array((string) $itemInfo['id_tree'], $userData['folders_list'], true);
675
676
                                // Also check restricted items if applicable
677
                                if (!$hasAccess && !empty($userData['restricted_items_list'])) {
678
                                    $restrictedItems = explode(',', $userData['restricted_items_list']);
679
                                    $hasAccess = in_array((string) $itemId, $restrictedItems, true);
680
                                }
681
682
                                if (!$hasAccess) {
683
                                    $strErrorDesc = 'Access denied to this item';
684
                                    $strErrorHeader = 'HTTP/1.1 403 Forbidden';
685
                                } else {
686
                                    // Validate at least one field to update is provided
687
                                    $updateableFields = ['label', 'password', 'description', 'login', 'email', 'url', 'tags', 'anyone_can_modify', 'icon', 'folder_id', 'totp'];
688
                                    $hasUpdateField = false;
689
                                    foreach ($updateableFields as $field) {
690
                                        if (isset($arrQueryStringParams[$field])) {
691
                                            $hasUpdateField = true;
692
                                            break;
693
                                        }
694
                                    }
695
696
                                    if (!$hasUpdateField) {
697
                                        $strErrorDesc = 'At least one field to update must be provided (label, password, description, login, email, url, tags, anyone_can_modify, icon, folder_id, totp)';
698
                                        $strErrorHeader = 'HTTP/1.1 400 Bad Request';
699
                                    } else {
700
                                        // Get user's private key for password encryption/decryption
701
                                        $userPrivateKey = $this->getUserPrivateKey($userData);
702
                                        if ($userPrivateKey === null) {
703
                                            $strErrorDesc = 'Invalid session or user keys not found';
704
                                            $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
705
                                        } else {
706
                                            // Update the item
707
                                            $itemModel = new ItemModel();
708
                                            $ret = $itemModel->updateItem(
709
                                                $itemId,
710
                                                $arrQueryStringParams,
0 ignored issues
show
It seems like $arrQueryStringParams can also be of type string; however, parameter $params of ItemModel::updateItem() does only seem to accept array, 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

710
                                                /** @scrutinizer ignore-type */ $arrQueryStringParams,
Loading history...
711
                                                $userData,
712
                                                $userPrivateKey
713
                                            );
714
715
                                            if ($ret['error'] === true) {
716
                                                $strErrorDesc = $ret['error_message'];
717
                                                $strErrorHeader = $ret['error_header'];
718
                                            } else {
719
                                                $responseData = json_encode($ret);
720
                                            }
721
                                        }
722
                                    }
723
                                }
724
                            }
725
                        } catch (Error $e) {
726
                            $strErrorDesc = $e->getMessage() . '. Something went wrong! Please contact support.';
727
                            $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
728
                        }
729
                    }
730
                } else {
731
                    $strErrorDesc = 'Data not consistent';
732
                    $strErrorHeader = 'HTTP/1.1 400 Bad Request - Expected array, received ' . gettype($arrQueryStringParams);
733
                }
734
            }
735
        } else {
736
            $strErrorDesc = 'Method not supported';
737
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
738
        }
739
740
        // send output
741
        if (empty($strErrorDesc) === true) {
742
            $this->sendOutput(
743
                $responseData,
744
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
745
            );
746
        } else {
747
            $this->sendOutput(
748
                json_encode(['error' => $strErrorDesc]),
749
                ['Content-Type: application/json', $strErrorHeader]
750
            );
751
        }
752
    }
753
    //end updateAction()
754
755
756
    /**
757
     * Delete an existing item
758
     * Updates an item based upon provided parameters and item ID
759
     *
760
     * @param array $userData User data from JWT token
761
     * @return void
762
     */
763
    public function deleteAction(array $userData): void
764
    {
765
        $request = symfonyRequest::createFromGlobals();
766
        $requestMethod = $request->getMethod();
767
        $strErrorDesc = $strErrorHeader = $responseData = '';
768
769
        if (strtoupper($requestMethod) === 'DELETE') {
770
            // Check if user is allowed to delete items
771
            if ((int) $userData['allowed_to_delete'] !== 1) {
772
                $strErrorDesc = 'User is not allowed to delete items';
773
                $strErrorHeader = 'HTTP/1.1 403 Forbidden';
774
            } else {
775
                // Get parameters
776
                $arrQueryStringParams = $this->getQueryStringParams();
777
                
778
                // Check that the parameters are indeed an array before using them
779
                if (is_array($arrQueryStringParams)) {
780
                    // Check if item ID is provided
781
                    if (!isset($arrQueryStringParams['id']) || empty($arrQueryStringParams['id'])) {
782
                        $strErrorDesc = 'Item ID is mandatory';
783
                        $strErrorHeader = 'HTTP/1.1 400 Bad Request';
784
                    } else {
785
                        $itemId = (int) $arrQueryStringParams['id'];
786
                        
787
                        try {
788
                            // Load item info to check access rights
789
                            $itemInfo = DB::queryFirstRow(
790
                                'SELECT id, id_tree, label FROM ' . prefixTable('items') . ' WHERE id = %i',
791
                                $itemId
792
                            );
793
794
                            if (DB::count() === 0) {
795
                                $strErrorDesc = 'Item not found';
796
                                $strErrorHeader = 'HTTP/1.1 404 Not Found';
797
                            } else {
798
                                // Check if user has access to the folder
799
                                $userFolders = explode(',', $userData['folders_list']) ?? [];
800
                                $hasAccess = in_array((string) $itemInfo['id_tree'], $userFolders, true);
801
802
                                // Also check restricted items if applicable
803
                                if (!$hasAccess && !empty($userData['restricted_items_list'])) {
804
                                    $restrictedItems = explode(',', $userData['restricted_items_list']) ?? [];
805
                                    $hasAccess = in_array((string) $itemId, $restrictedItems, true);
806
                                }
807
808
                                if (!$hasAccess) {
809
                                    $strErrorDesc = 'Access denied to this item';
810
                                    $strErrorHeader = 'HTTP/1.1 403 Forbidden';
811
                                } else {
812
                                    // delete the item
813
                                    $itemModel = new ItemModel();
814
                                    $ret = $itemModel->deleteItem(
815
                                        $itemId,
816
                                        $userData
817
                                    );
818
819
                                    if ($ret['error'] === true) {
820
                                        $strErrorDesc = $ret['error_message'];
821
                                        $strErrorHeader = $ret['error_header'];
822
                                    } else {
823
                                        $responseData = json_encode($ret);
824
                                    }
825
                                }
826
                            }
827
                        } catch (Error $e) {
828
                            $strErrorDesc = $e->getMessage() . '. Something went wrong! Please contact support.';
829
                            $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
830
                        }
831
                    }
832
                } else {
833
                    $strErrorDesc = 'Data not consistent';
834
                    $strErrorHeader = 'HTTP/1.1 400 Bad Request - Expected array, received ' . gettype($arrQueryStringParams);
835
                }
836
            }
837
        } else {
838
            $strErrorDesc = 'Method not supported';
839
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
840
        }
841
842
        // send output
843
        if (empty($strErrorDesc) === true) {
844
            $this->sendOutput(
845
                $responseData,
846
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
847
            );
848
        } else {
849
            $this->sendOutput(
850
                json_encode(['error' => $strErrorDesc]),
851
                ['Content-Type: application/json', $strErrorHeader]
852
            );
853
        }
854
    }
855
    //end deleteAction()
856
857
858
}
859