Passed
Push — master ( a3cfc6...b61ff7 )
by Nils
06:14
created

sendError()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 10
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
Best Practice introduced by
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
}