Issues (26)

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/downloadFile.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      downloadFile.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 voku\helper\AntiXSS;
33
use TeampassClasses\NestedTree\NestedTree;
34
use TeampassClasses\SessionManager\SessionManager;
35
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
36
use TeampassClasses\Language\Language;
37
use EZimuel\PHPSecureSession;
38
use TeampassClasses\PerformChecks\PerformChecks;
39
use TeampassClasses\ConfigManager\ConfigManager;
40
41
// Load functions
42
require_once 'main.functions.php';
43
44
// init
45
loadClasses('DB');
46
$session = SessionManager::getSession();
47
$request = SymfonyRequest::createFromGlobals();
48
$lang = new Language($session->get('user-language') ?? 'english');
49
$antiXss = new AntiXSS();
50
51
// Load config
52
$configManager = new ConfigManager();
53
$SETTINGS = $configManager->getAllSettings();
54
55
// Do checks
56
// Instantiate the class with posted data
57
$checkUserAccess = new PerformChecks(
58
    dataSanitizer(
59
        [
60
            'type' => htmlspecialchars($request->request->get('type', ''), ENT_QUOTES, 'UTF-8'),
61
        ],
62
        [
63
            'type' => 'trim|escape',
64
        ],
65
    ),
66
    [
67
        'user_id' => returnIfSet($session->get('user-id'), null),
68
        'user_key' => returnIfSet($session->get('key'), null),
69
    ]
70
);
71
// Handle the case
72
echo $checkUserAccess->caseHandler();
73
if (
74
    $checkUserAccess->userAccessPage('items') === false ||
75
    $checkUserAccess->checkSession() === false
76
) {
77
    // Not allowed page
78
    $session->set('system-error_code', ERR_NOT_ALLOWED);
79
    include $SETTINGS['cpassman_dir'] . '/error.php';
80
    exit;
81
}
82
83
// Define Timezone
84
date_default_timezone_set($SETTINGS['timezone'] ?? 'UTC');
85
86
// Set header properties
87
header('Content-type: text/html; charset=utf-8');
88
header('Cache-Control: no-cache, no-store, must-revalidate');
89
error_reporting(E_ERROR);
90
set_time_limit(0);
91
92
// --------------------------------- //
93
94
// Prepare GET variables
95
$getData = dataSanitizer(
96
    [
97
        'filename' => $request->query->get('name'),
98
        'fileid' => $request->query->get('fileid'),
99
        'action' => $request->query->get('action'),
100
        'file' => $request->query->get('file'),
101
        'key' => $request->query->get('key'),
102
        'key_tmp' => $request->query->get('key_tmp'),
103
        'pathIsFiles' => $request->query->get('pathIsFiles'),
104
    ],
105
    [
106
        'filename' => 'trim|escape',
107
        'fileid' => 'cast:integer',
108
        'action' => 'trim|escape',
109
        'file' => 'trim|escape',
110
        'key' => 'trim|escape',
111
        'key_tmp' => 'trim|escape',
112
        'pathIsFiles' => 'trim|escape',
113
    ]
114
);
115
116
/**
117
 * Send error response and exit
118
 */
119
function sendError($message) {
120
    // Clear any headers that might have been set
121
    if (!headers_sent()) {
122
        header_remove();
123
    }
124
    echo $message;
125
    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...
126
}
127
128
/**
129
 * Set download headers safely
130
 */
131
function setDownloadHeaders($filename, $filesize = null) {
132
    // Clean filename for header - avoid rawurldecode
133
    $safeFilename = str_replace('"', '\\"', basename($filename));
134
    
135
    header('Content-Description: File Transfer');
136
    header('Content-Type: application/octet-stream');
137
    header('Content-Disposition: attachment; filename="' . $safeFilename . '"');
138
    header('Cache-Control: must-revalidate, no-cache, no-store');
139
    header('Pragma: public');
140
    header('Expires: 0');
141
    
142
    if ($filesize !== null) {
143
        header('Content-Length: ' . $filesize);
144
    }
145
}
146
147
/**
148
 * Validate and secure file path
149
 */
150
function validateSecurePath($basePath, $filename) {
151
    if (empty($filename)) {
152
        return false;
153
    }
154
    
155
    $filepath = $basePath . '/' . basename($filename);
156
    
157
    // Security: Verify the resolved path is within the allowed directory
158
    $realBasePath = realpath($basePath);
159
    $realFilePath = realpath($filepath);
160
    
161
    if ($realFilePath === false || $realBasePath === false || 
162
        strpos($realFilePath, $realBasePath) !== 0) {
163
        return false;
164
    }
165
    
166
    return file_exists($filepath) && is_file($filepath) && is_readable($filepath) ? $filepath : false;
167
}
168
169
$get_filename = (string) $antiXss->xss_clean($getData['filename']);
170
$get_fileid = (int) $antiXss->xss_clean($getData['fileid']);
171
$get_pathIsFiles = (string) $antiXss->xss_clean($getData['pathIsFiles']);
172
$get_action = (string) $antiXss->xss_clean($getData['action']);
173
$get_file = (string) $antiXss->xss_clean($getData['file']);
174
$get_key = (string) $antiXss->xss_clean($getData['key']);
175
$get_key_tmp = (string) $antiXss->xss_clean($getData['key_tmp']);
176
177
// Branch 1: Files from files folder (pathIsFiles = 1)
178
if (null !== $get_pathIsFiles && (int) $get_pathIsFiles === 1) {
179
180
    // Clean filename
181
    $get_filename = str_replace(array("\r", "\n"), '', $get_filename);
182
    $get_filename = preg_replace('/[^a-zA-Z0-9_\.-]/', '', basename($get_filename));
183
    
184
    if (empty($get_filename)) {
185
        sendError('ERROR_Invalid_filename');
186
    }
187
    
188
    // Validate file path
189
    $filepath = validateSecurePath($SETTINGS['path_to_files_folder'], $get_filename);
190
    if (!$filepath) {
191
        sendError('ERROR_File_not_found');
192
    }
193
    
194
    // Check permissions based on action
195
    $hasAccess = false;
196
    if ($get_action === 'backup') {
197
        $hasAccess = userHasAccessToBackupFile(
198
            $session->get('user-id'), 
199
            $get_file, 
200
            $get_key, 
201
            $get_key_tmp
202
        );
203
    } else {
204
        $hasAccess = userHasAccessToFile($session->get('user-id'), $get_fileid);
205
    }
206
    
207
    if (!$hasAccess) {
208
        sendError('ERROR_Not_allowed');
209
    }
210
    
211
    // All checks passed - serve file
212
    setDownloadHeaders($get_filename, filesize($filepath));
213
    
214
    if (ob_get_level()) {
215
        ob_end_clean();
216
    }
217
    
218
    readfile($filepath);
219
    exit;
220
}
221
222
// Branch 2: Files from upload folder (encrypted/standard)
223
else {
224
    
225
    if (empty($get_fileid)) {
226
        sendError('ERROR_Invalid_fileid');
227
    }
228
    
229
    // Try to get encrypted file info first
230
    $file_info = DB::queryFirstRow(
231
        'SELECT f.id AS id, f.file AS file, f.name AS name, f.status AS status, f.extension AS extension,
232
        s.share_key AS share_key
233
        FROM ' . prefixTable('files') . ' AS f
234
        INNER JOIN ' . prefixTable('sharekeys_files') . ' AS s ON (f.id = s.object_id)
235
        WHERE s.user_id = %i AND s.object_id = %i',
236
        $session->get('user-id'),
237
        $get_fileid
238
    );
239
    
240
    $isEncrypted = (DB::count() > 0);
241
    $fileContent = '';
242
    
243
    if ($isEncrypted) {
244
        // Decrypt the file
245
        $fileContent = decryptFile(
246
            $file_info['file'],
247
            $SETTINGS['path_to_upload_folder'],
248
            decryptUserObjectKey($file_info['share_key'], $session->get('user-private_key'))
249
        );
250
    } else {
251
        // Get unencrypted file info
252
        $file_info = DB::queryFirstRow(
253
            'SELECT f.id AS id, f.file AS file, f.name AS name, f.status AS status, f.extension AS extension
254
            FROM ' . prefixTable('files') . ' AS f
255
            WHERE f.id = %i',
256
            $get_fileid
257
        );
258
        
259
        if (DB::count() === 0) {
260
            sendError('ERROR_No_file_found');
261
        }
262
    }
263
    
264
    // Prepare filename for download
265
    $filename = str_replace('b64:', '', $file_info['name']);
266
    $filename = basename($filename, '.' . $file_info['extension']);
267
    $filename = isBase64($filename) === true ? base64_decode($filename) : $filename;
268
    $filename = $filename . '.' . $file_info['extension'];
269
    
270
    // Determine file path
271
    $candidatePath1 = $SETTINGS['path_to_upload_folder'] . '/' . TP_FILE_PREFIX . $file_info['file'];
272
    $candidatePath2 = $SETTINGS['path_to_upload_folder'] . '/' . TP_FILE_PREFIX . base64_decode($file_info['file']);
273
    
274
    $filePath = false;
275
    if (file_exists($candidatePath1)) {
276
        $filePath = realpath($candidatePath1);
277
    } elseif (file_exists($candidatePath2)) {
278
        $filePath = realpath($candidatePath2);
279
    }
280
    
281
    if (WIP === true) {
282
        error_log('downloadFile.php: filePath: ' . $filePath . " - ");
283
    }
284
    
285
    // Validate file path and security
286
    $uploadFolderPath = realpath($SETTINGS['path_to_upload_folder']);
287
    if (!$filePath || !is_readable($filePath) || strpos($filePath, $uploadFolderPath) !== 0) {
288
        sendError('ERROR_No_file_found');
289
    }
290
    
291
    // Set headers and serve file
292
    setDownloadHeaders($filename, filesize($filePath));
293
    
294
    if (ob_get_level()) {
295
        ob_end_clean();
296
    }
297
    
298
    if (empty($fileContent)) {
299
        // Serve file directly from disk
300
        readfile($filePath);
301
    } elseif (is_string($fileContent)) {
302
        // Serve decrypted content
303
        echo base64_decode($fileContent);
304
    } else {
305
        sendError('ERROR_No_file_found');
306
    }
307
    
308
    exit;
309
}