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/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-2025 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 = '';
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 = ' WHERE 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
205
     */
206
    public function createAction(array $userData)
207
    {
208
        $request = symfonyRequest::createFromGlobals();
209
        $requestMethod = $request->getMethod();
210
        $strErrorDesc = $strErrorHeader = $responseData = '';
211
212
        if (strtoupper($requestMethod) === 'POST') {
213
            // Is user allowed to create a folder
214
            // We check if allowed_to_create
215
            if ((int) $userData['allowed_to_create'] !== 1) {
216
                $strErrorDesc = 'User is not allowed to create an item';
217
                $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
218
            } else {
219
                if (empty($userData['folders_list']) === false) {
220
                    $userData['folders_list'] = explode(',', $userData['folders_list']);
221
                } else {
222
                    $userData['folders_list'] = [];
223
                }
224
225
                // get parameters
226
                $arrQueryStringParams = $this->getQueryStringParams();
227
228
                // Check that the parameters are indeed an array before using them
229
                if (is_array($arrQueryStringParams)) {
230
                    // check parameters
231
                    $arrCheck = $this->checkNewItemData($arrQueryStringParams, $userData);
232
233
                    if ($arrCheck['error'] === true) {
234
                        $strErrorDesc = $arrCheck['strErrorDesc'];
235
                        $strErrorHeader = $arrCheck['strErrorHeader'];
236
                    } else {
237
                        // launch
238
                        $itemModel = new ItemModel();
239
                        $ret = $itemModel->addItem(
240
                            (int) $arrQueryStringParams['folder_id'],
241
                            (string) $arrQueryStringParams['label'],
242
                            (string) $arrQueryStringParams['password'],
243
                            (string) $arrQueryStringParams['description'] ?? '',
244
                            (string) $arrQueryStringParams['login'],
245
                            (string) $arrQueryStringParams['email'] ?? '',
246
                            (string) $arrQueryStringParams['url'] ?? '' ,
247
                            (string) $arrQueryStringParams['tags'] ?? '',
248
                            (int) $arrQueryStringParams['anyone_can_modify'] ?? 0,
249
                            (string) $arrQueryStringParams['icon'] ?? '',
250
                            (int) $userData['id'],
251
                            (string) $userData['username'],
252
                            (string) $userData['totp'],
253
                        );
254
                        $responseData = json_encode($ret);
255
                    }
256
                
257
                } else {
258
                    // Gérer le cas où les paramètres ne sont pas un tableau
259
                    $strErrorDesc = 'Data not consistent';
260
                    $strErrorHeader = 'Expected array, received ' . gettype($arrQueryStringParams);
261
                }
262
            }
263
        } else {
264
            $strErrorDesc = 'Method not supported';
265
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
266
        }
267
268
        // send output
269
        if (empty($strErrorDesc) === true) {
270
            $this->sendOutput(
271
                $responseData,
272
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
273
            );
274
        } else {
275
            $this->sendOutput(
276
                json_encode(['error' => $strErrorDesc]),
277
                ['Content-Type: application/json', $strErrorHeader]
278
            );
279
        }
280
    }
281
    //end addAction()
282
283
284
    /**
285
     * Manage case get - get an item
286
     *
287
     * @param array $userData
288
     */
289
    public function getAction(array $userData): void
290
    {
291
        
292
        $request = symfonyRequest::createFromGlobals();
293
        $requestMethod = $request->getMethod();
294
        $strErrorDesc = '';
295
        $sqlExtra = '';
296
        $sqlLimit = 0;
297
        $responseData = '';
298
        $strErrorHeader = '';
299
        $sql_constraint = ' AND (i.id_tree IN ('.$userData['folders_list'].')';
300
        if (!empty($userData['restricted_items_list'])) {
301
            $sql_constraint .= 'OR i.id IN ('.$userData['restricted_items_list'].')';
302
        }
303
        $sql_constraint .= ')';
304
305
        // get parameters
306
        $arrQueryStringParams = $this->getQueryStringParams();
307
308
        if (strtoupper($requestMethod) === 'GET') {
309
            // SQL where clause with item id
310
            if (isset($arrQueryStringParams['id']) === true) {
311
                // build sql where clause by ID
312
                $sqlExtra = ' WHERE i.id = '.$arrQueryStringParams['id'] . $sql_constraint;
313
            } else if (isset($arrQueryStringParams['label']) === true) {
314
                // build sql where clause by LABEL
315
                $sqlExtra = ' WHERE i.label '.(isset($arrQueryStringParams['like']) === true && (int) $arrQueryStringParams['like'] === 1 ? ' LIKE "%'.$arrQueryStringParams['label'].'%"' : ' = '.$arrQueryStringParams['label']) . $sql_constraint;
316
                $sqlLimit = isset($arrQueryStringParams['limit']) === true && (int) $arrQueryStringParams['limit'] > 0 ? $arrQueryStringParams['limit'] : 50;   // let's limit to 50 by default
317
            } else if (isset($arrQueryStringParams['description']) === true) {
318
                // build sql where clause by DESCRIPTION
319
                $sqlExtra = ' WHERE i.description '.(isset($arrQueryStringParams['like']) === true && (int) $arrQueryStringParams['like'] === 1 ? ' LIKE '.$arrQueryStringParams['description'] : ' = '.$arrQueryStringParams['description']).$sql_constraint;
320
            } else {
321
                // Send error
322
                $this->sendOutput(
323
                    json_encode(['error' => 'Item id, label or description is mandatory']),
324
                    ['Content-Type: application/json', 'HTTP/1.1 401 Expected parameters not provided']
325
                );
326
            }
327
328
            // send query
329
            try {
330
                $itemModel = new ItemModel();
331
332
                // Get user's private key from database
333
                $userPrivateKey = $this->getUserPrivateKey($userData);
334
                if ($userPrivateKey === null) {
335
                    $strErrorDesc = 'Invalid session or user keys not found';
336
                    $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
337
                } else {
338
                    $arrItems = $itemModel->getItems($sqlExtra, $sqlLimit, $userPrivateKey, $userData['id']);
339
                    $responseData = json_encode($arrItems);
340
                }
341
            } catch (Error $e) {
342
                $strErrorDesc = $e->getMessage().'. Something went wrong! Please contact support.6';
343
                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
344
            }
345
        } else {
346
            $strErrorDesc = 'Method not supported';
347
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
348
        }
349
350
        // send output
351
        if (empty($strErrorDesc) === true) {
352
            $this->sendOutput(
353
                $responseData,
354
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
355
            );
356
        } else {
357
            $this->sendOutput(
358
                json_encode(['error' => $strErrorDesc]), 
359
                ['Content-Type: application/json', $strErrorHeader]
360
            );
361
        }
362
    }
363
    //end getAction()
364
365
    /**
366
     * Find items by URL
367
     * Searches for items matching a specific URL
368
     *
369
     * @param array $userData User data from JWT token
370
     * @return void
371
     */
372
    public function findByUrlAction(array $userData): void
373
    {
374
        $request = symfonyRequest::createFromGlobals();
375
        $requestMethod = $request->getMethod();
376
        $strErrorDesc = $responseData = $strErrorHeader = '';
377
378
        // Get parameters
379
        $arrQueryStringParams = $this->getQueryStringParams();
380
381
        if (strtoupper($requestMethod) === 'GET') {
382
            // Check if URL parameter is provided
383
            if (isset($arrQueryStringParams['url']) === false || empty($arrQueryStringParams['url']) === true) {
384
                $this->sendOutput(
385
                    json_encode(['error' => 'URL parameter is mandatory']),
386
                    ['Content-Type: application/json', 'HTTP/1.1 400 Bad Request']
387
                );
388
                return;
389
            }
390
391
            // Prepare user's accessible folders
392
            /*if (empty($userData['folders_list']) === false) {
393
                $userData['folders_list'] = explode(',', $userData['folders_list']);
394
            } else {
395
                $userData['folders_list'] = [];
396
            }*/
397
398
            // Build SQL constraint for accessible folders
399
            $sql_constraint = ' AND (i.id_tree IN (' . $userData['folders_list'] . ')';
400
            if (!empty($userData['restricted_items_list'])) {
401
                $sql_constraint .= ' OR i.id IN (' . $userData['restricted_items_list'] . ')';
402
            }
403
            $sql_constraint .= ')';
404
405
            // Decode URL if needed
406
            $searchUrl = urldecode($arrQueryStringParams['url']);
407
408
            try {
409
                // Get user's private key from database
410
                $userPrivateKey = $this->getUserPrivateKey($userData);
411
                if ($userPrivateKey === null) {
412
                    $strErrorDesc = 'Invalid session or user keys not found';
413
                    $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
414
                } else {
415
                    // Query items with the specific URL
416
                    $rows = DB::query(
417
                        "SELECT i.id, i.label, i.login, i.url, i.id_tree, 
418
                                CASE WHEN o.enabled = 1 THEN 1 ELSE 0 END AS has_otp
419
                        FROM " . prefixTable('items') . " AS i
420
                        LEFT JOIN " . prefixTable('items_otp') . " AS o ON (o.item_id = i.id)
421
                        WHERE i.url LIKE %s" . $sql_constraint . "
422
                            AND i.deleted_at IS NULL
423
                        ORDER BY i.label ASC",
424
                        "%".$searchUrl."%"
425
                    );
426
427
                    $ret = [];
428
                    foreach ($rows as $row) {
429
                        // Get user's sharekey for this item
430
                        $shareKey = DB::queryfirstrow(
0 ignored issues
show
The assignment to $shareKey is dead and can be removed.
Loading history...
431
                            'SELECT share_key
432
                            FROM ' . prefixTable('sharekeys_items') . '
433
                            WHERE user_id = %i AND object_id = %i',
434
                            $userData['id'],
435
                            $row['id']
436
                        );
437
438
                        // Skip if no sharekey found (user doesn't have access)
439
                        if (DB::count() === 0) {
440
                            continue;
441
                        }
442
443
                        // Build response
444
                        array_push(
445
                            $ret,
446
                            [
447
                                'id' => (int) $row['id'],
448
                                'label' => $row['label'],
449
                                'login' => $row['login'],
450
                                'url' => $row['url'],
451
                                'folder_id' => (int) $row['id_tree'],
452
                                'has_otp' => (int) $row['has_otp'],
453
                            ]
454
                        );
455
                    }
456
457
                    if (!empty($ret)) {
458
                        $responseData = json_encode($ret);
459
                    } else {
460
                        $strErrorDesc = 'No items found with this URL';
461
                        $strErrorHeader = 'HTTP/1.1 204 No Content';
462
                    }
463
                }
464
            } catch (Error $e) {
465
                $strErrorDesc = $e->getMessage() . '. Something went wrong! Please contact support.';
466
                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
467
            }
468
        } else {
469
            $strErrorDesc = 'Method not supported';
470
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
471
        }
472
473
        // Send output
474
        if (empty($strErrorDesc) === true) {
475
            $this->sendOutput(
476
                $responseData,
477
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
478
            );
479
        } else {
480
            $this->sendOutput(
481
                json_encode(['error' => $strErrorDesc]),
482
                ['Content-Type: application/json', $strErrorHeader]
483
            );
484
        }
485
    }
486
    //end findByUrlAction()
487
488
489
    /**
490
     * Get OTP/TOTP code for an item
491
     * Retrieves the current 6-digit TOTP code for an item with OTP enabled
492
     *
493
     * @param array $userData User data from JWT token
494
     * @return void
495
     */
496
    public function getOtpAction(array $userData): void
497
    {
498
        $request = symfonyRequest::createFromGlobals();
499
        $requestMethod = $request->getMethod();
500
        $strErrorDesc = '';
501
        $responseData = '';
502
        $strErrorHeader = '';
503
504
        // get parameters
505
        $arrQueryStringParams = $this->getQueryStringParams();
506
507
        if (strtoupper($requestMethod) === 'GET') {
508
            // Check if item ID is provided
509
            if (!isset($arrQueryStringParams['id']) || empty($arrQueryStringParams['id'])) {
510
                $this->sendOutput(
511
                    json_encode(['error' => 'Item id is mandatory']),
512
                    ['Content-Type: application/json', 'HTTP/1.1 400 Bad Request']
513
                );
514
                return;
515
            }
516
517
            $itemId = (int) $arrQueryStringParams['id'];
518
519
            try {
520
                // Load config
521
                loadClasses('DB');
522
523
                // Load item basic info to check folder access
524
                $itemInfo = DB::queryFirstRow(
525
                    'SELECT id_tree FROM ' . prefixTable('items') . ' WHERE id = %i',
526
                    $itemId
527
                );
528
529
                if (DB::count() === 0) {
530
                    $strErrorDesc = 'Item not found';
531
                    $strErrorHeader = 'HTTP/1.1 404 Not Found';
532
                } else {
533
                    // Check if user has access to the folder
534
                    $userFolders = !empty($userData['folders_list']) ? explode(',', $userData['folders_list']) : [];
535
                    $hasAccess = in_array((string) $itemInfo['id_tree'], $userFolders, true);
536
537
                    // Also check restricted items if applicable
538
                    if (!$hasAccess && !empty($userData['restricted_items_list'])) {
539
                        $restrictedItems = explode(',', $userData['restricted_items_list']);
540
                        $hasAccess = in_array((string) $itemId, $restrictedItems, true);
541
                    }
542
543
                    if (!$hasAccess) {
544
                        $strErrorDesc = 'Access denied to this item';
545
                        $strErrorHeader = 'HTTP/1.1 403 Forbidden';
546
                    } else {
547
                        // Load OTP data
548
                        $otpData = DB::queryFirstRow(
549
                            'SELECT secret, enabled FROM ' . prefixTable('items_otp') . ' WHERE item_id = %i',
550
                            $itemId
551
                        );
552
553
                        if (DB::count() === 0) {
554
                            $strErrorDesc = 'OTP not configured for this item';
555
                            $strErrorHeader = 'HTTP/1.1 404 Not Found';
556
                        } elseif ((int) $otpData['enabled'] !== 1) {
557
                            $strErrorDesc = 'OTP is not enabled for this item';
558
                            $strErrorHeader = 'HTTP/1.1 403 Forbidden';
559
                        } else {
560
                            // Decrypt the secret
561
                            $decryptedSecret = cryption(
562
                                $otpData['secret'],
563
                                '',
564
                                'decrypt'
565
                            );
566
567
                            if (isset($decryptedSecret['string']) && !empty($decryptedSecret['string'])) {
568
                                // Generate OTP code using OTPHP library
569
                                try {
570
                                    $otp = \OTPHP\TOTP::createFromSecret($decryptedSecret['string']);
571
                                    $otpCode = $otp->now();
572
                                    $otpExpiresIn = $otp->expiresIn();
573
574
                                    $responseData = json_encode([
575
                                        'otp_code' => $otpCode,
576
                                        'expires_in' => $otpExpiresIn,
577
                                        'item_id' => $itemId
578
                                    ]);
579
                                } catch (\RuntimeException $e) {
580
                                    $strErrorDesc = 'Failed to generate OTP code: ' . $e->getMessage();
581
                                    $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
582
                                }
583
                            } else {
584
                                $strErrorDesc = 'Failed to decrypt OTP secret';
585
                                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
586
                            }
587
                        }
588
                    }
589
                }
590
            } catch (\Error $e) {
591
                $strErrorDesc = $e->getMessage() . '. Something went wrong! Please contact support.';
592
                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
593
            }
594
        } else {
595
            $strErrorDesc = 'Method not supported';
596
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
597
        }
598
599
        // send output
600
        if (empty($strErrorDesc) === true) {
601
            $this->sendOutput(
602
                $responseData,
603
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
604
            );
605
        } else {
606
            $this->sendOutput(
607
                json_encode(['error' => $strErrorDesc]),
608
                ['Content-Type: application/json', $strErrorHeader]
609
            );
610
        }
611
    }
612
    //end getOtpAction()
613
614
615
    /**
616
     * Update an existing item
617
     * Updates an item based upon provided parameters and item ID
618
     *
619
     * @param array $userData User data from JWT token
620
     * @return void
621
     */
622
    public function updateAction(array $userData): void
623
    {
624
        $request = symfonyRequest::createFromGlobals();
625
        $requestMethod = $request->getMethod();
626
        $strErrorDesc = $strErrorHeader = $responseData = '';
627
628
        if (strtoupper($requestMethod) === 'PUT' || strtoupper($requestMethod) === 'POST') {
629
            // Check if user is allowed to update items
630
            if ((int) $userData['allowed_to_update'] !== 1) {
631
                $strErrorDesc = 'User is not allowed to update items';
632
                $strErrorHeader = 'HTTP/1.1 403 Forbidden';
633
            } else {
634
                // Prepare user's accessible folders
635
                if (empty($userData['folders_list']) === false) {
636
                    $userData['folders_list'] = explode(',', $userData['folders_list']);
637
                } else {
638
                    $userData['folders_list'] = [];
639
                }
640
641
                // Get parameters
642
                $arrQueryStringParams = $this->getQueryStringParams();
643
644
                // Check that the parameters are indeed an array before using them
645
                if (is_array($arrQueryStringParams)) {
646
                    // Check if item ID is provided
647
                    if (!isset($arrQueryStringParams['id']) || empty($arrQueryStringParams['id'])) {
648
                        $strErrorDesc = 'Item ID is mandatory';
649
                        $strErrorHeader = 'HTTP/1.1 400 Bad Request';
650
                    } else {
651
                        $itemId = (int) $arrQueryStringParams['id'];
652
653
                        try {
654
                            // Load item info to check access rights
655
                            $itemInfo = DB::queryFirstRow(
656
                                'SELECT id, id_tree, label FROM ' . prefixTable('items') . ' WHERE id = %i',
657
                                $itemId
658
                            );
659
660
                            if (DB::count() === 0) {
661
                                $strErrorDesc = 'Item not found';
662
                                $strErrorHeader = 'HTTP/1.1 404 Not Found';
663
                            } else {
664
                                // Check if user has access to the folder
665
                                $hasAccess = in_array((string) $itemInfo['id_tree'], $userData['folders_list'], true);
666
667
                                // Also check restricted items if applicable
668
                                if (!$hasAccess && !empty($userData['restricted_items_list'])) {
669
                                    $restrictedItems = explode(',', $userData['restricted_items_list']);
670
                                    $hasAccess = in_array((string) $itemId, $restrictedItems, true);
671
                                }
672
673
                                if (!$hasAccess) {
674
                                    $strErrorDesc = 'Access denied to this item';
675
                                    $strErrorHeader = 'HTTP/1.1 403 Forbidden';
676
                                } else {
677
                                    // Validate at least one field to update is provided
678
                                    $updateableFields = ['label', 'password', 'description', 'login', 'email', 'url', 'tags', 'anyone_can_modify', 'icon', 'folder_id', 'totp'];
679
                                    $hasUpdateField = false;
680
                                    foreach ($updateableFields as $field) {
681
                                        if (isset($arrQueryStringParams[$field])) {
682
                                            $hasUpdateField = true;
683
                                            break;
684
                                        }
685
                                    }
686
687
                                    if (!$hasUpdateField) {
688
                                        $strErrorDesc = 'At least one field to update must be provided (label, password, description, login, email, url, tags, anyone_can_modify, icon, folder_id, totp)';
689
                                        $strErrorHeader = 'HTTP/1.1 400 Bad Request';
690
                                    } else {
691
                                        // Get user's private key for password encryption/decryption
692
                                        $userPrivateKey = $this->getUserPrivateKey($userData);
693
                                        if ($userPrivateKey === null) {
694
                                            $strErrorDesc = 'Invalid session or user keys not found';
695
                                            $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
696
                                        } else {
697
                                            // Update the item
698
                                            $itemModel = new ItemModel();
699
                                            $ret = $itemModel->updateItem(
700
                                                $itemId,
701
                                                $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

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