Skip to content

Commit 621dd96

Browse files
committed
feat: add max file size limit for hashing functionality
1 parent d4d6b00 commit 621dd96

File tree

3 files changed

+77
-19
lines changed

3 files changed

+77
-19
lines changed

Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM php:8.5-fpm-alpine AS base
22

3-
ENV DIRBROWSER_VERSION=4.0.0
3+
ENV DIRBROWSER_VERSION=4.1.0
44

55
RUN apk update && apk upgrade
66

@@ -57,6 +57,8 @@ ENV DATE_FORMAT=relative
5757

5858
ENV HASH=true
5959

60+
ENV HASH_MAX_FILE_SIZE_MB=100
61+
6062
ENV TRANSITION=false
6163

6264
ENV HASH_FOLDER=false

docs/docs/configuration/hashes.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,8 @@ This feature should not be used to restrict access as the hash is publicly avail
4040
To protect the file use the [password protection](password.mdx) feature.
4141
:::
4242

43-
:::danger
44-
If you are hosting large files (1GB+) hashing them may take a significant amount of time and CPU resources on the server potentially leading to a Denial of Service for other users. Consinder disabling this feature in this case `HASH=false`.
45-
:::
4643

4744
import EnvConfig from '@site/src/components/EnvConfig';
4845

4946
<!-- <EnvConfig name="HASH" init="true" values="true,false"/> -->
50-
<EnvConfig name="HASH|HASH_REQUIRED|HASH_ALGO" init="true|false|sha256" values="true,false|true,false|md2,md4,md5,sha1,sha224,sha256,sha384,sha512/224,sha512/256,sha512,sha3-224,sha3-256,sha3-384,sha3-512,ripemd128,ripemd160,ripemd256,ripemd320,whirlpool,snefru,snefru256,gost,gost-crypto,adler32,crc32,crc32b,crc32c,fnv132,fnv1a32,fnv164,fnv1a64,joaat,murmur3a,murmur3c,murmur3f,xxh32,xxh64,xxh3,xxh128" desc="|Hash is always required|" versions="3.0|3.3|3.1" />
47+
<EnvConfig name="HASH|HASH_MAX_FILE_SIZE_MB|HASH_REQUIRED|HASH_ALGO" init="true|100|false|sha256" values="true,false|integer|true,false|md2,md4,md5,sha1,sha224,sha256,sha384,sha512/224,sha512/256,sha512,sha3-224,sha3-256,sha3-384,sha3-512,ripemd128,ripemd160,ripemd256,ripemd320,whirlpool,snefru,snefru256,gost,gost-crypto,adler32,crc32,crc32b,crc32c,fnv132,fnv1a32,fnv164,fnv1a64,joaat,murmur3a,murmur3c,murmur3f,xxh32,xxh64,xxh3,xxh128" desc="|Hash is always required|" versions="3.0|3.3|3.1" />

src/index.php

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,26 @@ function safe_utf8(string $input): string
5252
return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $input) ?? '';
5353
}
5454

55+
function hash_max_file_size_bytes(): ?int
56+
{
57+
$raw = getenv('HASH_MAX_FILE_SIZE_MB');
58+
if ($raw === false || $raw === '') return null;
59+
if (!is_numeric($raw)) return null;
60+
$mb = floatval($raw);
61+
if ($mb <= 0) return null;
62+
$bytes = (int) floor($mb * 1024 * 1024);
63+
return $bytes > 0 ? $bytes : null;
64+
}
65+
66+
function hashing_allowed_for_file(string $path): bool
67+
{
68+
$maxBytes = hash_max_file_size_bytes();
69+
if ($maxBytes === null) return true;
70+
$size = @filesize($path);
71+
if ($size === false) return false;
72+
return $size <= $maxBytes;
73+
}
74+
5575
// fix whitespace in path results in not found errors
5676
$request_uri = rawurldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
5777

@@ -506,6 +526,15 @@ function downloadBatch(array $urls) {
506526
$[if `process.env.HASH`]$
507527
// only allow download if requested hash matches actual hash
508528
if (${{`process.env.HASH_REQUIRED === "true" ? "true ||" : ""`}}$ isset($_REQUEST["hash"]) || isset($meta) && $meta->hash_required === true) {
529+
if (!hashing_allowed_for_file($local_path)) {
530+
http_response_code(413);
531+
$limitMb = getenv('HASH_MAX_FILE_SIZE_MB');
532+
die("<b>Hashing disabled.</b> File exceeds HASH_MAX_FILE_SIZE_MB (" . htmlspecialchars((string) $limitMb) . " MB).");
533+
}
534+
if (!isset($_REQUEST["hash"])) {
535+
http_response_code(403);
536+
die("<b>Access denied.</b> Hash is required for this file.");
537+
}
509538
if ($_REQUEST["hash"] !== hash_file('${{`process.env.HASH_ALGO`}}$', $local_path)) {
510539
http_response_code(403);
511540
die("<b>Access denied.</b> Supplied hash does not match actual file hash.");
@@ -521,14 +550,22 @@ function downloadBatch(array $urls) {
521550

522551
$[if `process.env.API === "true"`]$
523552
if(isset($_REQUEST["info"])) {
553+
$hash_value = null;
554+
$[end]$
555+
$[if `process.env.API === "true" && process.env.HASH === "true"`]$
556+
if (hashing_allowed_for_file($local_path)) {
557+
$hash_value = hash_file('${{`process.env.HASH_ALGO`}}$', $local_path);
558+
}
559+
$[end]$
560+
$[if `process.env.API === "true"`]$
524561
$info = [
525562
"url" => $relative_path, // FIXME: use host domain! abc.de/foobar
526563
"name" => basename($local_path),
527564
"mime" => mime_content_type($local_path) ?? "application/octet-stream",
528565
"size" => filesize($local_path),
529566
"modified" => filemtime($local_path),
530567
"downloads" => ${{`process.env.DOWNLOAD_COUNTER === "true" ? "intval($redis->get($relative_path))" : "0"`}}$,
531-
"hash_${{`process.env.HASH_ALGO`}}$" => ${{`process.env.HASH === "true" ? "hash_file('"+process.env.HASH_ALGO+"', $local_path)" : "null"`}}$
568+
"hash_${{`process.env.HASH_ALGO`}}$" => $hash_value
532569
];
533570
header("Content-Type: application/json");
534571
die(json_encode($info, JSON_UNESCAPED_SLASHES));
@@ -1155,11 +1192,20 @@ function getRelativeTimeString(date, lang = navigator.language) {
11551192

11561193
$[if `process.env.HASH === "true"`]$
11571194
// via api bc otherwise we need to include the hash in the tree itself which is costly
1195+
const HASH_MAX_FILE_SIZE_MB = Number('${{`process.env.HASH_MAX_FILE_SIZE_MB ?? ""`}}$');
1196+
const HASH_MAX_FILE_SIZE_BYTES = Number.isFinite(HASH_MAX_FILE_SIZE_MB) && HASH_MAX_FILE_SIZE_MB > 0
1197+
? Math.floor(HASH_MAX_FILE_SIZE_MB * 1024 * 1024)
1198+
: null;
1199+
11581200
const getHashViaApi = async (url) => {
11591201
const res = await fetch(url);
1160-
if (!res.ok) throw new Error('Hash request failed');
1202+
if (!res.ok) {
1203+
if (res.status === 413) throw new Error('too_large');
1204+
throw new Error('request_failed');
1205+
}
11611206
const data = await res.json();
11621207
const hash = data.hash_${{`process.env.HASH_ALGO`}}$;
1208+
if (hash === null || hash === undefined || String(hash).length === 0) throw new Error('unavailable');
11631209
await copyTextToClipboard(String(hash ?? ''));
11641210
}
11651211
$[end]$
@@ -1489,18 +1535,31 @@ function getRelativeTimeString(date, lang = navigator.language) {
14891535
$[if `process.env.LAYOUT === "popup" && process.env.HASH === "true"`]$
14901536
// Show hash entry; actual hash is fetched on demand via API.
14911537
if (typeof getHashViaApi === 'function') {
1492-
const link = document.createElement('a');
1493-
link.href = '#';
1494-
link.className = 'link-primary link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover';
1495-
link.textContent = 'Click to calculate hash';
1496-
link.addEventListener('click', async (e) => {
1497-
link.textContent = 'Calculating…';
1498-
e.preventDefault();
1499-
if (!popupState.apiInfoUrl) return;
1500-
await getHashViaApi(popupState.apiInfoUrl);
1501-
link.textContent = 'Hash copied to clipboard';
1502-
});
1503-
entries.push({ label: 'Hash', value: link });
1538+
const fileSize = Number(payload.size ?? 0);
1539+
const hashingTooLarge = HASH_MAX_FILE_SIZE_BYTES !== null && Number.isFinite(fileSize) && fileSize > HASH_MAX_FILE_SIZE_BYTES;
1540+
if (hashingTooLarge) {
1541+
const note = document.createElement('span');
1542+
note.className = 'text-body-secondary';
1543+
note.textContent = `Too large to hash (>${HASH_MAX_FILE_SIZE_MB} MB)`;
1544+
entries.push({ label: 'Hash', value: note });
1545+
} else {
1546+
const link = document.createElement('a');
1547+
link.href = '#';
1548+
link.className = 'link-primary link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover';
1549+
link.textContent = 'Click to calculate hash';
1550+
link.addEventListener('click', async (e) => {
1551+
link.textContent = 'Calculating…';
1552+
e.preventDefault();
1553+
if (!popupState.apiInfoUrl) return;
1554+
try {
1555+
await getHashViaApi(popupState.apiInfoUrl);
1556+
link.textContent = 'Hash copied to clipboard';
1557+
} catch (err) {
1558+
link.textContent = err && String(err.message) === 'too_large' ? 'Too large to hash' : 'Hash unavailable';
1559+
}
1560+
});
1561+
entries.push({ label: 'Hash', value: link });
1562+
}
15041563
}
15051564
$[end]$
15061565
$[if `process.env.LAYOUT === "popup"`]$

0 commit comments

Comments
 (0)