Completed
Push — development ( b096dd...68d2c9 )
by Nils
07:26
created

upload.attachments.php ➔ handleAttachmentError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @file          upload.attachments.php
4
 * @author        Nils Laumaillé
5
 * @version       2.1.27
6
 * @copyright     (c) 2009-2017 Nils Laumaillé
7
 * @licensing     GNU AFFERO GPL 3.0
8
 * @link          http://www.teampass.net
9
 *
10
 * This library is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13
 */
14
15
require_once('../SecureHandler.php');
16
session_start();
17
if (
18
    !isset($_SESSION['CPM']) || $_SESSION['CPM'] != 1 ||
19
    !isset($_SESSION['user_id']) || empty($_SESSION['user_id']) ||
20
    !isset($_SESSION['key']) || empty($_SESSION['key'])
21
) {
22
    die('Hacking attempt...');
23
}
24
25
/* do checks */
26
require_once '../checks.php';
27 View Code Duplication
if (!checkUser($_SESSION['user_id'], $_SESSION['key'], "items")) {
28
    $_SESSION['error']['code'] = ERR_NOT_ALLOWED; //not allowed page
29
    handleAttachmentError('Not allowed to ...', 110);
30
    exit();
31
}
32
33
//check for session
34 View Code Duplication
if (isset($_POST['PHPSESSID'])) {
35
    session_id($_POST['PHPSESSID']);
36
} elseif (isset($_GET['PHPSESSID'])) {
37
    session_id($_GET['PHPSESSID']);
38
} else {
39
    handleAttachmentError('No Session was found.', 110);
40
}
41
42
43
// Get parameters
44
$chunk = isset($_REQUEST["chunk"]) ? intval($_REQUEST["chunk"]) : 0;
45
$chunks = isset($_REQUEST["chunks"]) ? intval($_REQUEST["chunks"]) : 0;
46
$fileName = isset($_REQUEST["name"]) ? $_REQUEST["name"] : '';
47
48
49
// token check
50
if (!isset($_POST['user_token'])) {
51
    handleAttachmentError('No user token found.', 110);
52
    exit();
53
} else {
54
    //Connect to mysql server
55
    require_once '../../includes/config/settings.php';
56
    require_once '../../includes/libraries/Database/Meekrodb/db.class.php';
57
    DB::$host = $server;
58
    DB::$user = $user;
59
    DB::$password = $pass;
60
    DB::$dbName = $database;
61
    DB::$port = $port;
62
    DB::$encoding = $encoding;
63
    DB::$error_handler = true;
64
    $link = mysqli_connect($server, $user, $pass, $database, $port);
65
    $link->set_charset($encoding);
66
67
    // delete expired tokens
68
    DB::delete(prefix_table("tokens"), "end_timestamp < %i", time());
69
70
    if (isset($_SESSION[$_POST['user_token']]) && ($chunk < $chunks - 1) && $_SESSION[$_POST['user_token']] >= 0) {
71
        // increase end_timestamp for token
72
        DB::update(
73
            prefix_table('tokens'),
74
            array(
75
                'end_timestamp' => time() + 10
76
                ),
77
            "user_id = %i AND token = %s",
78
            $_SESSION['user_id'],
79
            $_POST['user_token']
80
        );
81
    } else {
82
83
        // create a session if several files to upload
84
        if (!isset($_SESSION[$_POST['user_token']]) || empty($_SESSION[$_POST['user_token']]) || $_SESSION[$_POST['user_token']] === 0) {
85
            $_SESSION[$_POST['user_token']] = $_POST['files_number'];
86
        } else if ($_SESSION[$_POST['user_token']] > 0) {
87
            // increase end_timestamp for token
88
            DB::update(
89
                prefix_table('tokens'),
90
                array(
91
                    'end_timestamp' => time() + 30
92
                    ),
93
                "user_id = %i AND token = %s",
94
                $_SESSION['user_id'],
95
                $_POST['user_token']
96
            );
97
            // decrease counter of files to upload
98
            $_SESSION[$_POST['user_token']]--;
99
        } else {
100
            // no more files to upload, kill session
101
            unset($_SESSION[$_POST['user_token']]);
102
            handleAttachmentError('No user token found.', 110);
103
            die();
104
        }
105
106
        // check if token is expired
107
        $data = DB::queryFirstRow(
108
            "SELECT end_timestamp FROM ".prefix_table("tokens")." WHERE user_id = %i AND token = %s",
109
            $_SESSION['user_id'],
110
            $_POST['user_token']
111
        );
112
        // clear user token
113
        if ($_SESSION[$_POST['user_token']] === 0) {
114
            DB::delete(prefix_table("tokens"), "user_id = %i AND token = %s", $_SESSION['user_id'], $_POST['user_token']);
115
            unset($_SESSION[$_POST['user_token']]);
116
        }
117
118
        if (time() <= $data['end_timestamp']) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
119
            // it is ok
120
        } else {
121
            // too old
122
            unset($_SESSION[$_POST['user_token']]);
123
            handleAttachmentError('User token expired.', 110);
124
            die();
125
        }
126
    }
127
}
128
129
// HTTP headers for no cache etc
130
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
131
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
132
header("Cache-Control: no-store, no-cache, must-revalidate");
133
header("Cache-Control: post-check=0, pre-check=0", false);
134
header("Pragma: no-cache");
135
136
// load functions
137
require_once $_SESSION['settings']['cpassman_dir'].'/sources/main.functions.php';
138
139
$targetDir = $_SESSION['settings']['path_to_upload_folder'];
140
141
$cleanupTargetDir = true; // Remove old files
142
$maxFileAge = 5 * 3600; // Temp file age in seconds
143
$valid_chars_regex = 'A-Za-z0-9'; //accept only those characters
144
$MAX_FILENAME_LENGTH = 260;
145
$max_file_size_in_bytes = 2147483647; //2Go
146
147
date_default_timezone_set($_POST['timezone']);
148
149
// Check post_max_size
150
$POST_MAX_SIZE = ini_get('post_max_size');
151
$unit = strtoupper(substr($POST_MAX_SIZE, -1));
152
$multiplier = ($unit == 'M' ? 1048576 : ($unit == 'K' ? 1024 : ($unit == 'G' ? 1073741824 : 1)));
153
if ((int) $_SERVER['CONTENT_LENGTH'] > $multiplier * (int) $POST_MAX_SIZE && $POST_MAX_SIZE) {
154
    handleAttachmentError('POST exceeded maximum allowed size.', 111);
155
}
156
157
// Validate the file size (Warning: the largest files supported by this code is 2GB)
158
$file_size = @filesize($_FILES['file']['tmp_name']);
159
if (!$file_size || $file_size > $max_file_size_in_bytes) {
160
    handleAttachmentError('File exceeds the maximum allowed size', 120);
161
}
162
if ($file_size <= 0) {
163
    handleAttachmentError('File size outside allowed lower bound', 112);
164
}
165
166
// Validate the upload
167
if (!isset($_FILES['file'])) {
168
    handleAttachmentError('No upload found in $_FILES for Filedata', 121);
169
} elseif (isset($_FILES['file']['error']) && $_FILES['file']['error'] != 0) {
170
    handleAttachmentError($uploadErrors[$_FILES['Filedata']['error']], 122);
171 View Code Duplication
} elseif (!isset($_FILES['file']['tmp_name']) || !@is_uploaded_file($_FILES['file']['tmp_name'])) {
172
    handleAttachmentError('Upload failed is_uploaded_file test.', 123);
173
} elseif (!isset($_FILES['file']['name'])) {
174
    handleAttachmentError('File has no name.', 113);
175
}
176
177
// Validate file name (for our purposes we'll just remove invalid characters)
178
$file_name = preg_replace('[^'.$valid_chars_regex.']', '', strtolower(basename($_FILES['file']['name'])));
179 View Code Duplication
if (strlen($file_name) == 0 || strlen($file_name) > $MAX_FILENAME_LENGTH) {
180
    handleAttachmentError('Invalid file name: '.$file_name.'.', 114);
181
}
182
183
// Validate file extension
184
$ext = strtolower(getFileExtension($_REQUEST["name"]));
185 View Code Duplication
if (!in_array(
186
    $ext,
187
    explode(
188
        ',',
189
        $_SESSION['settings']['upload_docext'].','.$_SESSION['settings']['upload_imagesext'].
190
        ','.$_SESSION['settings']['upload_pkgext'].','.$_SESSION['settings']['upload_otherext']
191
    )
192
)) {
193
    handleAttachmentError('Invalid file extension.', 115);
194
}
195
196
// 5 minutes execution time
197
set_time_limit(5 * 60);
198
199
// Clean the fileName for security reasons
200
$fileName = preg_replace('/[^\w\._]+/', '_', $fileName);
201
$fileName = preg_replace('[^'.$valid_chars_regex.']', '', strtolower(basename($fileName)));
202
203
// Make sure the fileName is unique but only if chunking is disabled
204 View Code Duplication
if ($chunks < 2 && file_exists($targetDir.DIRECTORY_SEPARATOR.$fileName)) {
205
    $ext = strrpos($fileName, '.');
206
    $fileNameA = substr($fileName, 0, $ext);
207
    $fileNameB = substr($fileName, $ext);
208
209
    $count = 1;
210
    while (file_exists($targetDir.DIRECTORY_SEPARATOR.$fileNameA.'_'.$count.$fileNameB)) {
211
        $count++;
212
    }
213
214
    $fileName = $fileNameA.'_'.$count.$fileNameB;
215
}
216
217
$filePath = $targetDir.DIRECTORY_SEPARATOR.$fileName;
218
219
// Create target dir
220
if (!file_exists($targetDir)) {
221
    try {
222
        mkdir($targetDir);
0 ignored issues
show
Security File Manipulation introduced by
$targetDir can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_POST, and $_SESSION is assigned
    in sources/upload/upload.attachments.php on line 85
  2. $targetDir is assigned
    in sources/upload/upload.attachments.php on line 139

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
223
    } catch(Exception $e){
224
        print_r($e);
225
    }
226
}
227
228
// Remove old temp files
229 View Code Duplication
if ($cleanupTargetDir && is_dir($targetDir) && ($dir = opendir($targetDir))) {
0 ignored issues
show
Security File Exposure introduced by
$targetDir can contain request data and is used in file inclusion context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_POST, and $_SESSION is assigned
    in sources/upload/upload.attachments.php on line 85
  2. $targetDir is assigned
    in sources/upload/upload.attachments.php on line 139

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
230
    while (($file = readdir($dir)) !== false) {
231
        $tmpfilePath = $targetDir.DIRECTORY_SEPARATOR.$file;
232
233
        // Remove temp file if it is older than the max age and is not the current file
234
        if (preg_match('/\.part$/', $file)
235
            && (filemtime($tmpfilePath) < time() - $maxFileAge)
236
            && ($tmpfilePath != "{$filePath}.part")
237
        ) {
238
            try {
239
                unlink($tmpfilePath);
0 ignored issues
show
Security File Manipulation introduced by
$tmpfilePath can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_POST, and $_SESSION is assigned
    in sources/upload/upload.attachments.php on line 85
  2. $targetDir is assigned
    in sources/upload/upload.attachments.php on line 139
  3. $tmpfilePath is assigned
    in sources/upload/upload.attachments.php on line 231

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
240
            } catch(Exception $e){
241
                print_r($e);
242
            }
243
        }
244
    }
245
246
    closedir($dir);
247
} else {
248
    die('{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}');
249
}
250
251
// Look for the content type header
252
if (isset($_SERVER["HTTP_CONTENT_TYPE"])) {
253
    $contentType = $_SERVER["HTTP_CONTENT_TYPE"];
254
}
255
256
if (isset($_SERVER["CONTENT_TYPE"])) {
257
    $contentType = $_SERVER["CONTENT_TYPE"];
258
}
259
260
// should we encrypt the attachment?
261
if (isset($_SESSION['settings']['enable_attachment_encryption']) && $_SESSION['settings']['enable_attachment_encryption'] == 1) {
262
    // get key
263
    if (empty($ascii_key)) {
264
        $ascii_key = file_get_contents(SECUREPATH."/teampass-seckey.txt");
265
    }
266
267
    // prepare encryption of attachment
268
    $iv = substr(hash("md5", "iv".$ascii_key), 0, 8);
269
    $key = substr(hash("md5", "ssapmeat1".$ascii_key, true), 0, 24);
270
    $opts = array('iv'=>$iv, 'key'=>$key);
271
272
    $file_status = "encrypted";
273
} else {
274
    $file_status = "clear";
275
}
276
277
// Handle non multipart uploads older WebKit versions didn't support multipart in HTML5
278
if (strpos($contentType, "multipart") !== false) {
279
    if (isset($_FILES['file']['tmp_name']) && is_uploaded_file($_FILES['file']['tmp_name'])) {
280
        // Open temp file
281
        $out = fopen("{$filePath}.part", $chunk == 0 ? "wb" : "ab");
282
283 View Code Duplication
        if (isset($_SESSION['settings']['enable_attachment_encryption']) && $_SESSION['settings']['enable_attachment_encryption'] === "1") {
284
            // Add the Mcrypt stream filter
285
            stream_filter_append($out, 'mcrypt.tripledes', STREAM_FILTER_WRITE, $opts);
286
        }
287
288 View Code Duplication
        if ($out) {
289
            // Read binary input stream and append it to temp file
290
            $in = fopen($_FILES['file']['tmp_name'], "rb");
291
292
            if ($in) {
293
                while ($buff = fread($in, 4096)) {
294
                    fwrite($out, $buff);
295
                }
296
            } else {
297
                die(
298
                    '{"jsonrpc" : "2.0",
299
                    "error" : {"code": 101, "message": "Failed to open input stream."},
300
                    "id" : "id"}'
301
                );
302
            }
303
            fclose($in);
304
            fclose($out);
305
            try {
306
                unlink($_FILES['file']['tmp_name']);
307
            } catch(Exception $e){
308
                print_r($e);
309
            }
310
        } else {
311
            die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
312
        }
313
    } else {
314
        die('{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}');
315
    }
316
} else {
317
    // Open temp file
318
    $out = fopen("{$filePath}.part", $chunk == 0 ? "wb" : "ab");
319
320 View Code Duplication
    if (isset($_SESSION['settings']['enable_attachment_encryption']) && $_SESSION['settings']['enable_attachment_encryption'] === "1") {
321
        // Add the Mcrypt stream filter
322
        stream_filter_append($out, 'mcrypt.tripledes', STREAM_FILTER_WRITE, $opts);
323
    }
324
325 View Code Duplication
    if ($out) {
326
        // Read binary input stream and append it to temp file
327
        $in = fopen("php://input", "rb");
328
329
        if ($in) {
330
            while ($buff = fread($in, 4096)) {
331
                fwrite($out, $buff);
332
            }
333
        } else {
334
            die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}');
335
        }
336
        fclose($in);
337
        fclose($out);
338
    } else {
339
        die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
340
    }
341
}
342
343
// Check if file has been uploaded
344
if (!$chunks || $chunk == $chunks - 1) {
345
    // Strip the temp .part suffix off
346
    rename("{$filePath}.part", $filePath);
0 ignored issues
show
Security File Manipulation introduced by
$filePath can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_POST, and $_SESSION is assigned
    in sources/upload/upload.attachments.php on line 85
  2. $targetDir is assigned
    in sources/upload/upload.attachments.php on line 139
  3. $filePath is assigned
    in sources/upload/upload.attachments.php on line 217

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
347
} else {
348
    // continue uploading other chunks
349
    die();
350
}
351
352
// Get some variables
353
$fileRandomId = md5($fileName.time());
354
rename($filePath, $targetDir.DIRECTORY_SEPARATOR.$fileRandomId);
0 ignored issues
show
Security File Manipulation introduced by
$filePath can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_POST, and $_SESSION is assigned
    in sources/upload/upload.attachments.php on line 85
  2. $targetDir is assigned
    in sources/upload/upload.attachments.php on line 139
  3. $filePath is assigned
    in sources/upload/upload.attachments.php on line 217

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
Security File Manipulation introduced by
$targetDir . DIRECTORY_SEPARATOR . $fileRandomId can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_POST, and $_SESSION is assigned
    in sources/upload/upload.attachments.php on line 85
  2. $targetDir is assigned
    in sources/upload/upload.attachments.php on line 139

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
355
356
357
// Case ITEM ATTACHMENTS - Store to database
358
if (isset($_POST['edit_item']) && $_POST['type_upload'] == "item_attachments") {
359
    DB::insert(
360
        $pre.'files',
361
        array(
362
            'id_item' => $_POST['itemId'],
363
            'name' => $fileName,
364
            'size' => $_FILES['file']['size'],
365
            'extension' => getFileExtension($fileName),
366
            'type' => $_FILES['file']['type'],
367
            'file' => $fileRandomId,
368
            'status' => $file_status
369
        )
370
    );
371
    // Log upload into databse only if "item edition"
372
    if (isset($_POST['edit_item']) && $_POST['edit_item'] === true) {
373
        DB::insert(
374
            $pre.'log_items',
375
            array(
376
                'id_item' => $_POST['itemId'],
377
                'date' => time(),
378
                'id_user' => $_SESSION['user_id'],
379
                'action' => 'at_modification',
380
                'raison' => 'at_add_file : '.addslashes($fileName)
381
            )
382
        );
383
    }
384
}
385
386
// Return JSON-RPC response
387
die('{"jsonrpc" : "2.0", "result" : null, "id" : "id"}');
388
389
390
/* Handles the error output. */
391
function handleAttachmentError($message, $code)
392
{
393
    echo '{"jsonrpc" : "2.0", "error" : {"code": '.$code.', "message": "'.$message.'"}, "id" : "id"}';
0 ignored issues
show
Security Cross-Site Scripting introduced by
'{"jsonrpc" : "2.0", "er...ge . '"}, "id" : "id"}' can contain request data and is used in output context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_FILES, and $_FILES['file']['name'] is escaped by basename() for file context(s), and basename($_FILES['file']['name']) is passed through strtolower(), and strtolower(basename($_FILES['file']['name'])) is passed through preg_replace(), and $file_name is assigned
    in sources/upload/upload.attachments.php on line 178
  2. 'Invalid file name: ' . $file_name . '.' is passed to handleAttachmentError()
    in sources/upload/upload.attachments.php on line 180

Preventing Cross-Site-Scripting Attacks

Cross-Site-Scripting allows an attacker to inject malicious code into your website - in particular Javascript code, and have that code executed with the privileges of a visiting user. This can be used to obtain data, or perform actions on behalf of that visiting user.

In order to prevent this, make sure to escape all user-provided data:

// for HTML
$sanitized = htmlentities($tainted, ENT_QUOTES);

// for URLs
$sanitized = urlencode($tainted);

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
394
    exit(0);
395
}