Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ location ^~ /images/ {
}
```

### Upload size limits

Uploads are limited by PHP: `upload_max_filesize` (the largest single file) and
`post_max_size` (the largest request, which must be larger than the file). W2
shows a message naming the limit that was hit. Web servers have limits of their
own, which apply first and are not detected by W2: `client_max_body_size` in
nginx (default 1 MB) and `LimitRequestBody` in Apache. Raise them together, e.g.
`client_max_body_size 20m;` for `upload_max_filesize = 16M` and `post_max_size = 20M`.

### SVG uploads

SVG files can contain scripts, which would run in the context of the wiki when
Expand Down
16 changes: 15 additions & 1 deletion index.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,18 @@ function destroy_session()
// Main code

$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : 'view';
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST' && (int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 0 && !$_POST && !$_FILES)
{
// PHP discards the whole body of a request larger than post_max_size (no token, no file)
http_response_code(413);
die(sprintf(__('The upload is too large: the server accepts requests of up to %s (post_max_size).'), h(ini_get('post_max_size'))).' '.
__('Nothing was uploaded or saved. Please go back and try again with a smaller file.'));
}
if (in_array($action, array('save', 'uploaded', 'renamed', 'deleted', 'imgRenamed', 'imgDeleted'), true) &&
($_SERVER['REQUEST_METHOD'] !== 'POST' || !isValidCSRFToken($_POST['csrf_token'] ?? null)))
{
http_response_code(403);
die('Invalid request: missing or wrong security token (or uploaded file too large). Please go back, reload the page and try again.');
die('Invalid request: missing or wrong security token. Please go back, reload the page and try again.');
}
if ($action === 'logout' && !isValidCSRFToken($_GET['csrf_token'] ?? null))
{
Expand Down Expand Up @@ -536,6 +543,13 @@ function destroy_session()
{
die('Invalid access. Uploads are disabled in the configuration.');
}
$uploadError = $_FILES['userfile']['error'] ?? UPLOAD_ERR_NO_FILE;
if ( $uploadError === UPLOAD_ERR_INI_SIZE || $uploadError === UPLOAD_ERR_FORM_SIZE )
{
$limit = ini_get('upload_max_filesize');
redirectWithMessage(requireValidPreviousPage('prevpage'), sprintf(
__('Upload error: the file is larger than the allowed %s (upload_max_filesize).'), h($limit)));
}
$tmpName = $_FILES['userfile']['tmp_name'];
$dstName = sanitizeFilename($_FILES['userfile']['name']);
$dstName = str_replace(" ", "_", $dstName); // image display currently doesn't like spaces!
Expand Down
3 changes: 3 additions & 0 deletions locales/de.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
// Messages
'Upload error' => 'Fehler beim Hochladen',
'Upload error: invalid file type' => 'Fehler beim Hochladen: Dieser Dateityp ist nicht zugelassen, bitte wende Dich an deinen Administrator!',
'The upload is too large: the server accepts requests of up to %s (post_max_size).' => 'Der Upload ist zu groß: Der Server akzeptiert Anfragen bis zu %s (post_max_size).',
'Nothing was uploaded or saved. Please go back and try again with a smaller file.' => 'Es wurde nichts hochgeladen oder gespeichert. Bitte gehe zurück und versuche es mit einer kleineren Datei.',
'Upload error: the file is larger than the allowed %s (upload_max_filesize).' => 'Fehler beim Hochladen: Die Datei ist größer als die erlaubten %s (upload_max_filesize).',
'Image uploading has been disabled on this installation.' => 'Hochladen ist nicht erlaubt, bitte wende Dich an deinen Administrator!',
'Creating new page since no page with given title exists!' => 'Es wird eine neue Seite angelegt, weil noch keine Seite mit dem angegebenen Titel existiert!',
'Updated links in the following pages:' => 'Es wurden Links in den folgenden Seiten aktualisiert:',
Expand Down
3 changes: 3 additions & 0 deletions locales/en.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
// Messages
'Upload error' => 'Upload error',
'Upload error: invalid file type' => 'Upload error: invalid file type',
'The upload is too large: the server accepts requests of up to %s (post_max_size).' => 'The upload is too large: the server accepts requests of up to %s (post_max_size).',
'Nothing was uploaded or saved. Please go back and try again with a smaller file.' => 'Nothing was uploaded or saved. Please go back and try again with a smaller file.',
'Upload error: the file is larger than the allowed %s (upload_max_filesize).' => 'Upload error: the file is larger than the allowed %s (upload_max_filesize).',
'Image uploading has been disabled on this installation.' => 'Image uploading has been disabled on this installation.',
'Restored unsaved draft from %s.' => 'Restored unsaved draft from %s.',
'Warning: the page was changed since this draft was started.' => 'Warning: the page was changed since this draft was started.',
Expand Down
55 changes: 55 additions & 0 deletions tests/Integration/UploadSizeLimitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace W2\Tests\Integration;

use W2\Tests\Support\AppTestCase;

/** Uploads larger than the PHP limits post_max_size and upload_max_filesize */
final class UploadSizeLimitTest extends AppTestCase
{
protected function serverOptions(): array
{
return ['phpIni' => ['post_max_size' => '100K', 'upload_max_filesize' => '40K']];
}

/** a GIF with padding after the image data, which is still a valid image */
private static function gifOfSize(int $bytes): string
{
return self::gif() . str_repeat("\0", $bytes - strlen(self::gif()));
}

public function testUploadAboveUploadMaxFilesizeIsRefusedWithTheLimit(): void
{
$pagesBefore = $this->server->pageFiles();
$note = $this->noteAfter($this->upload('big.gif', self::gifOfSize(60 * 1024)));
$this->assertStringContainsString('Upload error: the file is larger than the allowed 40K (upload_max_filesize)', $note);
$this->assertStringNotContainsString('error #', $note);
$this->assertSame([], $this->uploadedFiles());
$this->assertSame($pagesBefore, $this->server->pageFiles());
}

public function testUploadAbovePostMaxSizeIsRefusedWithTheLimit(): void
{
$this->allowPhpErrors = true; // PHP logs "POST Content-Length exceeds the limit"
$pagesBefore = $this->server->pageFiles();
$response = $this->upload('huge.gif', self::gifOfSize(300 * 1024));
$this->assertSame(413, $response->status);
$this->assertStringContainsString('The upload is too large', $response->body);
$this->assertStringContainsString('100K (post_max_size)', $response->body);
$this->assertStringNotContainsString('security token', $response->body);
$this->assertSame([], $this->uploadedFiles());
$this->assertSame($pagesBefore, $this->server->pageFiles());
}

public function testUploadBelowTheLimitsStillWorks(): void
{
$this->assertStringContainsString("File 'ok.gif' uploaded", $this->noteAfter($this->upload('ok.gif', self::gifOfSize(10 * 1024))));
$this->assertSame(['ok.gif'], $this->uploadedFiles());
}

public function testMissingTokenIsStillRefusedAsForbidden(): void
{
$response = $this->http->post('/index.php', ['action' => 'save', 'page' => 'X', 'content' => 'x']);
$this->assertSame(403, $response->status);
}
}
6 changes: 5 additions & 1 deletion tests/Support/AppServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ final class AppServer
* user.email), see initGit(); "gitRemote": also create a local bare
* repository as "origin" (implies "git");
* "pagesFolder": name of the pages folder (default "pages"), e.g. with
* spaces and quotes, which is set as PAGES_PATH
* spaces and quotes, which is set as PAGES_PATH;
* "phpIni": PHP ini values for the server, e.g. ['post_max_size' => '100K']
*/
public static function get(array $overrides = [], array $options = []): self
{
Expand Down Expand Up @@ -284,6 +285,9 @@ private function start(): void
'-d', 'display_errors=0', '-d', 'log_errors=1', '-d', 'error_reporting=-1',
'-d', 'opcache.enable=0', '-d', "session.save_path=$this->dir/sessions",
];
foreach ($this->options['phpIni'] ?? [] as $name => $value) {
array_push($command, '-d', "$name=$value");
}
$this->process = proc_open(
$command,
[0 => ['file', '/dev/null', 'r'], 1 => ['file', $this->logFile, 'a'], 2 => ['file', $this->logFile, 'a']],
Expand Down
Loading