Issues (15)

Security Analysis    no vulnerabilities found

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

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

sources/upload.attachments.php (1 issue)

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This file is part of the TeamPass project.
9
 * 
10
 * TeamPass is free software: you can redistribute it and/or modify it
11
 * under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, version 3 of the License.
13
 * 
14
 * TeamPass is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 * 
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 * 
22
 * Certain components of this file may be under different licenses. For
23
 * details, see the `licenses` directory or individual file headers.
24
 * ---
25
 * @file      upload.attachments.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-2025 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
use Symfony\Component\HttpFoundation\Request as RequestLocal;
33
use TeampassClasses\SessionManager\SessionManager;
34
use TeampassClasses\Language\Language;
35
use TeampassClasses\PerformChecks\PerformChecks;
36
use TeampassClasses\ConfigManager\ConfigManager;
37
38
39
// Load functions
40
require_once 'main.functions.php';
41
$session = SessionManager::getSession();
42
$request = RequestLocal::createFromGlobals();
43
// init
44
loadClasses('DB');
45
$lang = new Language();
46
47
// Load config if $SETTINGS not defined
48
if (empty($SETTINGS)) {
49
    $configManager = new ConfigManager();
50
    $SETTINGS = $configManager->getAllSettings();
51
}
52
53
// Do checks
54
// Instantiate the class with posted data
55
$checkUserAccess = new PerformChecks(
56
    dataSanitizer(
57
        [
58
            'type' => htmlspecialchars($request->request->get('type', ''), ENT_QUOTES, 'UTF-8'),
59
        ],
60
        [
61
            'type' => 'trim|escape',
62
        ],
63
    ),
64
    [
65
        'user_id' => returnIfSet($session->get('user-id'), null),
66
        'user_key' => returnIfSet($session->get('key'), null),
67
    ]
68
);
69
// Handle the case
70
echo $checkUserAccess->caseHandler();
71
if (
72
    $checkUserAccess->userAccessPage('items') === false ||
73
    $checkUserAccess->checkSession() === false
74
) {
75
    // Not allowed page
76
    $session->set('system-error_code', ERR_NOT_ALLOWED);
77
    include $SETTINGS['cpassman_dir'] . '/error.php';
78
    exit;
79
}
80
81
// Define Timezone
82
date_default_timezone_set($SETTINGS['timezone'] ?? 'UTC');
83
84
// Set header properties
85
header('Content-type: text/html; charset=utf-8');
86
header('Cache-Control: no-cache, no-store, must-revalidate');
87
error_reporting(E_ERROR);
88
set_time_limit(0);
89
90
// --------------------------------- //
91
92
//check for session
93
if (null !== $request->request->filter('PHPSESSID', null, FILTER_SANITIZE_FULL_SPECIAL_CHARS)) {
94
    session_id($request->request->filter('PHPSESSID', null, FILTER_SANITIZE_FULL_SPECIAL_CHARS));
95
} elseif (null !== $request->query->get('PHPSESSID')) {
96
    session_id(filter_var($request->query->get('PHPSESSID'), FILTER_SANITIZE_FULL_SPECIAL_CHARS));
97
} else {
98
    handleAttachmentError('No Session was found.', 100);
99
}
100
101
// Prepare POST variables
102
$post_user_token = $request->request->filter('user_upload_token', null, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
103
$post_type_upload = $request->request->filter('type_upload', null, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
104
$post_itemId = $request->request->filter('itemId', null, FILTER_SANITIZE_NUMBER_INT);
105
$post_files_number = $request->request->filter('files_number', null, FILTER_SANITIZE_NUMBER_INT);
106
$post_timezone = $request->request->filter('timezone', null, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
107
$post_isNewItem = $request->request->filter('isNewItem', null, FILTER_SANITIZE_NUMBER_INT);
108
$post_randomId = $request->request->filter('randomId', null, FILTER_SANITIZE_NUMBER_INT);
109
$post_isPersonal = $request->request->filter('isPersonal', null, FILTER_SANITIZE_NUMBER_INT);
110
$post_fileSize= $request->request->filter('file_size', null, FILTER_SANITIZE_NUMBER_INT);
111
$chunk = $request->request->filter('chunk', 0, FILTER_SANITIZE_NUMBER_INT);
112
$chunks = $request->request->filter('chunks', 0, FILTER_SANITIZE_NUMBER_INT);
113
$fileName = $request->request->filter('name', '', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
114
115
// token check
116
if (null === $post_user_token) {
117
    handleAttachmentError('No user token found.', 110);
118
    exit();
119
} else {
120
    // Check post_max_size
121
    $POST_MAX_SIZE = ini_get('post_max_size');
122
    $unit = strtoupper(substr(trim($POST_MAX_SIZE), -1)); // Assurez-vous de bien gérer les espaces éventuels
123
    $units = ['G' => 1073741824, 'M' => 1048576, 'K' => 1024];
124
    $multiplier = $units[$unit] ?? 1; // Vérifie si l'unité est dans le tableau, sinon 1
125
    $maxSize = (int)$POST_MAX_SIZE * $multiplier;
126
    
127
    // CHeck if the POST is too big
128
    if (!empty($_SERVER['CONTENT_LENGTH']) && (int)$_SERVER['CONTENT_LENGTH'] > $maxSize && $maxSize > 0) {
129
        handleAttachmentError('POST exceeded maximum allowed size.', 111, 413);
130
    }
131
132
    // CHeck if file size is too big
133
    if ($post_fileSize > $maxSize && $maxSize > 0) {
134
        handleAttachmentError('File exceeds the maximum allowed size', 120, 413);
135
        die();
136
    }
137
    if (WIP === true) error_log('POST_MAX_SIZE: ' . $POST_MAX_SIZE." - CONTENT_LENGTH: ".$_SERVER['CONTENT_LENGTH']." - UNIT: ".$unit." - MAX: ".$maxSize." - MULTIPLIER: ".$multiplier." - FILE_SIZE: ".$post_fileSize);
138
    
139
    // delete expired tokens
140
    DB::delete(prefixTable('tokens'), 'end_timestamp < %i', time());
141
142
    if (
143
        null !== $session->get($post_user_token)
144
        && ($chunk < $chunks - 1)
145
        && $session->get($post_user_token) >= 0
146
    ) {
147
        // increase end_timestamp for token
148
        DB::update(
149
            prefixTable('tokens'),
150
            array(
151
                'end_timestamp' => time() + 10,
152
            ),
153
            'user_id = %i AND token = %s',
154
            $session->get('user-id'),
155
            $post_user_token
156
        );
157
    } else {
158
        // create a session if several files to upload
159
        if (
160
            null === $session->get($post_user_token)
161
            || empty($session->get($post_user_token)) === true
162
            || (int) $session->get($post_user_token) === 0
163
        ) {
164
            $session->set($post_user_token, $post_files_number);
165
        } elseif ((int) $session->get($post_user_token) > 0) {
166
            // increase end_timestamp for token
167
            DB::update(
168
                prefixTable('tokens'),
169
                array(
170
                    'end_timestamp' => time() + 30,
171
                ),
172
                'user_id = %i AND token = %s',
173
                $session->get('user-id'),
174
                $post_user_token
175
            );
176
            // decrease counter of files to upload
177
            $session->set($post_user_token, $session->get($post_user_token) - 1);
178
        } else {
179
            // no more files to upload, kill session
180
            $session->remove($post_user_token);
181
            handleAttachmentError('No user token found.', 110);
182
            die();
183
        }
184
185
        // check if token is expired
186
        $data = DB::queryFirstRow(
187
            'SELECT end_timestamp
188
            FROM ' . prefixTable('tokens') . '
189
            WHERE user_id = %i AND token = %s',
190
            $session->get('user-id'),
191
            $post_user_token
192
        );
193
        // clear user token
194
        if ((int) $session->get($post_user_token) === 0) {
195
            DB::delete(
196
                prefixTable('tokens'),
197
                'user_id = %i AND token = %s',
198
                $session->get('user-id'),
199
                $post_user_token
200
            );
201
            $session->remove($post_user_token);
202
        }
203
204
        if (time() > $data['end_timestamp']) {
205
            // too old
206
            $session->remove($post_user_token);
207
            handleAttachmentError('User token expired.', 110);
208
            die();
209
        }
210
    }
211
212
    // Load Settings
213
    if (empty($SETTINGS)) {
214
        $configManager = new ConfigManager();
215
        $SETTINGS = $configManager->getAllSettings();
216
    }
217
}
218
219
// HTTP headers for no cache etc
220
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
221
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
222
header('Cache-Control: no-store, no-cache, must-revalidate');
223
header('Cache-Control: post-check=0, pre-check=0', false);
224
225
$targetDir = $SETTINGS['path_to_upload_folder'];
226
227
$cleanupTargetDir = true; // Remove old files
228
$maxFileAge = 5 * 3600; // Temp file age in seconds
229
$MAX_FILENAME_LENGTH = 260;
230
$max_file_size_in_bytes = 2147483647; //2Go
231
232
if (null !== $post_timezone) {
233
    date_default_timezone_set($post_timezone);
234
}
235
236
// Validate the file size (Warning: the largest files supported by this code is 2GB)
237
$file_size = @filesize($_FILES['file']['tmp_name']);
238
if ($file_size === false || (int) $file_size > (int) $max_file_size_in_bytes) {
239
    handleAttachmentError('File exceeds the maximum allowed size', 120, 413);
240
}
241
if ($file_size <= 0) {
242
    handleAttachmentError('File size outside allowed lower bound', 112);
243
}
244
245
// Validate the upload
246
if (!isset($_FILES['file'])) {
247
    handleAttachmentError('No upload found in $_FILES for Filedata', 121);
248
} elseif (isset($_FILES['file']['error']) && $_FILES['file']['error'] != 0) {
249
    handleAttachmentError($_FILES['Filedata']['error'], 122);
250
} elseif (!isset($_FILES['file']['tmp_name']) || !@is_uploaded_file($_FILES['file']['tmp_name'])) {
251
    handleAttachmentError('Upload failed is_uploaded_file test.', 123);
252
} elseif (!isset($_FILES['file']['name'])) {
253
    handleAttachmentError('File has no name.', 113);
254
}
255
256
// Validate file name (for our purposes we'll just remove invalid characters)
257
$file_name = preg_replace('[^A-Za-z0-9]', '', strtolower(basename($_FILES['file']['name'])));
258
if (strlen($file_name) == 0 || strlen($file_name) > $MAX_FILENAME_LENGTH) {
259
    handleAttachmentError('Invalid file name: ' . $file_name . '.', 114);
260
}
261
262
// Validate file extension
263
$ext = strtolower(getFileExtension($_REQUEST['name']));
264
265
// Check if we should enforce extensions
266
if (($SETTINGS['upload_all_extensions_file'] ?? '0') !== '1') {
267
    if (
268
        in_array(
269
            $ext,
270
            explode(
271
                ',',
272
                $SETTINGS['upload_docext'] . ',' . $SETTINGS['upload_imagesext'] .
273
                    ',' . $SETTINGS['upload_pkgext'] . ',' . $SETTINGS['upload_otherext']
274
            )
275
        ) === false
276
    ) {
277
        handleAttachmentError('Invalid file extension.', 115, 415);
278
    }
279
}
280
281
// 5 minutes execution time
282
set_time_limit(5 * 60);
283
284
// Clean the fileName for security reasons
285
$fileInfo = pathinfo($fileName);
286
$fileName = base64_encode($fileInfo['filename']) . '.' . $fileInfo['extension'];
287
$fileFullSize = 0;
288
289
// Make sure the fileName is unique but only if chunking is disabled
290
if ($chunks < 2 && file_exists($targetDir . DIRECTORY_SEPARATOR . $fileName)) {
291
    $ext = strrpos($fileName, '.');
292
    $fileNameA = substr($fileName, 0, $ext);
293
    $fileNameB = substr($fileName, $ext);
294
295
    $count = 1;
296
    while (file_exists($targetDir . DIRECTORY_SEPARATOR . $fileNameA . '_' . $count . $fileNameB)) {
297
        ++$count;
298
    }
299
300
    $fileName = $fileNameA . '_' . $count . $fileNameB;
301
}
302
303
$filePath = $targetDir . DIRECTORY_SEPARATOR . $fileName;
304
305
// Create target dir
306
if (file_exists($targetDir) === false) {
307
    try {
308
        mkdir($targetDir, 0777, true);
309
    } catch (Exception $e) {
310
        print_r($e);
311
    }
312
}
313
314
// Remove old temp files
315
if ($cleanupTargetDir && is_dir($targetDir) && ($dir = opendir($targetDir))) {
316
    while (($file = readdir($dir)) !== false) {
317
        $tmpfilePath = $targetDir . DIRECTORY_SEPARATOR . $file;
318
319
        // Remove temp file if it is older than the max age and is not the current file
320
        if (
321
            preg_match('/\.part$/', $file)
322
            && (filemtime($tmpfilePath) < time() - $maxFileAge)
323
            && ($tmpfilePath != "{$filePath}.part")
324
        ) {
325
            fileDelete($tmpfilePath, $SETTINGS);
326
        }
327
    }
328
329
    closedir($dir);
330
} else {
331
    die('{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}');
332
}
333
334
// Look for the content type header
335
$contentType = $_SERVER['CONTENT_TYPE']
336
    ?? $_SERVER['HTTP_CONTENT_TYPE']
337
    ?? '';
338
339
// Handle non multipart uploads older WebKit versions didn't support multipart in HTML5
340
if (strpos($contentType, 'multipart') !== false) {
341
    if (isset($_FILES['file']['tmp_name']) === true && is_uploaded_file($_FILES['file']['tmp_name']) === true) {
342
        // Open temp file
343
        $out = fopen("{$filePath}.part", $chunk == 0 ? 'wb' : 'ab');
344
345
        if ($out !== false) {
346
            // Read binary input stream and append it to temp file
347
            $in = fopen($_FILES['file']['tmp_name'], 'rb');
348
            $fileFullSize += (int) $_FILES['file']['size'];
349
350
            if ($in !== false) {
351
                while ($buff = fread($in, 4096)) {
352
                    fwrite($out, $buff);
353
                }
354
            } else {
355
                die('{"jsonrpc" : "2.0",
356
                    "error" : {"code": 101, "message": "Failed to open input stream."},
357
                    "id" : "id"}');
358
            }
359
            fclose($in);
360
            fclose($out);
361
362
            fileDelete($_FILES['file']['tmp_name'], $SETTINGS);
363
        } else {
364
            die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
365
        }
366
    } else {
367
        die('{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}');
368
    }
369
} else {
370
    // Open temp file
371
    $out = fopen("{$filePath}.part", $chunk == 0 ? 'wb' : 'ab');
372
373
    if ($out !== false) {
374
        // Read binary input stream and append it to temp file
375
        $in = fopen('php://input', 'rb');
376
377
        if ($in !== false) {
378
            while ($buff = fread($in, 4096)) {
379
                fwrite($out, $buff);
380
            }
381
        } else {
382
            die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}');
383
        }
384
        fclose($in);
385
        fclose($out);
386
    } else {
387
        die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
388
    }
389
}
390
391
// Check if file has been uploaded
392
if (!$chunks || $chunk == $chunks - 1) {
393
    // Strip the temp .part suffix off
394
    rename("{$filePath}.part", $filePath);
395
} else {
396
    // continue uploading other chunks
397
    die();
398
}
399
400
// Encrypt the file if requested
401
$newFile = encryptFile($fileName, $targetDir);
402
$newID = 0;
403
404
// Case ITEM ATTACHMENTS - Store to database
405
if (null !== $post_type_upload && $post_type_upload === 'item_attachments') {
406
    // Check case of new item
407
    if (
408
        isset($post_isNewItem) === true
409
        && (int) $post_isNewItem === 1
410
        && empty($post_randomId) === false
411
    ) {
412
        $post_itemId = $post_randomId;
413
    }
414
415
    DB::insert(
416
        prefixTable('files'),
417
        array(
418
            'id_item' => $post_itemId,
419
            'name' => 'b64:' . $fileName,   // add "b64:" prefix to indicate that the file name is base64 encoded
420
            'size' => $post_fileSize,
421
            'extension' => $fileInfo['extension'],
422
            'type' => $_FILES['file']['type'],
423
            'file' => $newFile['fileHash'],
424
            'status' => TP_ENCRYPTION_NAME,
425
        )
426
    );
427
    $newID = DB::insertId();
428
429
    // Store the key for users
430
    // Is this item a personal one?
431
    if ((int) $post_isPersonal === 0) {
432
        // It is not a personal item objectKey
433
        // This is a public object
434
        $users = DB::query(
435
            'SELECT id, public_key
436
            FROM ' . prefixTable('users') . '
437
            WHERE id NOT IN ("' . OTV_USER_ID . '","' . SSH_USER_ID . '","' . API_USER_ID . '")
438
            AND public_key != ""'
439
        );
440
        foreach ($users as $user) {
441
            // Insert in DB the new object key for this item by user
442
            DB::insert(
443
                prefixTable('sharekeys_files'),
444
                array(
445
                    'object_id' => $newID,
446
                    'user_id' => (int) $user['id'],
447
                    'share_key' => encryptUserObjectKey($newFile['objectKey'], $user['public_key']),
448
                )
449
            );
450
        }
451
    } else {
452
        DB::insert(
453
            prefixTable('sharekeys_files'),
454
            array(
455
                'object_id' => (int) $newID,
456
                'user_id' => (int) $session->get('user-id'),
457
                'share_key' => encryptUserObjectKey($newFile['objectKey'], $session->get('user-public_key')),
458
            )
459
        );
460
    }
461
462
    // Log upload into databse
463
    if ($post_isNewItem === false || (int) $post_isNewItem !== 1) {
464
        DB::insert(
465
            prefixTable('log_items'),
466
            array(
467
                'id_item' => $post_itemId,
468
                'date' => time(),
469
                'id_user' => $session->get('user-id'),
470
                'action' => 'at_modification',
471
                'raison' => 'at_add_file : ' . $fileName . ':' . $newID,
472
            )
473
        );
474
    }
475
}
476
477
// Return JSON-RPC response
478
die('{"jsonrpc" : "2.0", "result" : null, "id" : "' . $newID . '"}');
479
480
/**
481
 * Handle errors and kill script.
482
 *
483
 * @param string $message Message
484
 * 
485
 * @param integer $code    Code
486
 * 
487
 */
488
function handleAttachmentError($message, $code, $http_code = 400)
489
{
490
    // HTTP 40x code to avoid "success" in UI.
491
    http_response_code($http_code);
492
493
    // json error message
494
    echo json_encode([
495
        'jsonrpc' => '2.0',
496
        'error' => [
497
            'code' => $code,
498
            'message' => $message
499
        ],
500
        'id' => 'id'
501
    ]);
502
    
503
    // Force exit to avoid bypass filters.
504
    exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
505
}
506