fileName now take the upload code
This commit is contained in:
@@ -270,10 +270,12 @@ paths or stack traces.
|
|||||||
(salted), never in plaintext. Successful entry mints a short-lived HMAC-signed
|
(salted), never in plaintext. Successful entry mints a short-lived HMAC-signed
|
||||||
cookie, so access is granted by the server and cannot be forged.
|
cookie, so access is granted by the server and cannot be forged.
|
||||||
- **Path safety.** Filesystem paths are built only from server-generated,
|
- **Path safety.** Filesystem paths are built only from server-generated,
|
||||||
regex-validated ids (`^[a-z0-9]{12}$`). The original filename is metadata
|
regex-validated ids (`^[a-z0-9]{8}$`). The original filename is metadata
|
||||||
only and is sanitized (basename, control/null bytes stripped) before being
|
only and is sanitized (basename, control/null bytes stripped) before being
|
||||||
stored or emitted in `Content-Disposition`. Path traversal, `../`, encoded
|
stored; downloads are sent as `<upload-key>.<extension>`, so the basename in
|
||||||
traversal, null bytes, and symlink injection via user input are not possible.
|
`Content-Disposition` is always the server-generated key. Path traversal,
|
||||||
|
`../`, encoded traversal, null bytes, and symlink injection via user input are
|
||||||
|
not possible.
|
||||||
Downloads additionally refuse symlinks (`lstat`) as defense in depth.
|
Downloads additionally refuse symlinks (`lstat`) as defense in depth.
|
||||||
- **MIME / XSS.** Downloads are sent as `Content-Disposition: attachment` (browsers
|
- **MIME / XSS.** Downloads are sent as `Content-Disposition: attachment` (browsers
|
||||||
download rather than render) with `X-Content-Type-Options: nosniff`, so a spoofed
|
download rather than render) with `X-Content-Type-Options: nosniff`, so a spoofed
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { lstat } from "node:fs/promises";
|
|||||||
import { hasValidAuth, renderPasswordForm } from "../auth";
|
import { hasValidAuth, renderPasswordForm } from "../auth";
|
||||||
import { notFound } from "../http";
|
import { notFound } from "../http";
|
||||||
import { log } from "../log";
|
import { log } from "../log";
|
||||||
import { contentDisposition, fileIdFromKey, KEY_PATTERN, secretMatches } from "../security";
|
import { contentDisposition, downloadFileName, fileIdFromKey, KEY_PATTERN, secretMatches } from "../security";
|
||||||
import { isExpired, type Storage } from "../storage";
|
import { isExpired, type Storage } from "../storage";
|
||||||
|
|
||||||
export async function handleDownload(
|
export async function handleDownload(
|
||||||
@@ -41,7 +41,7 @@ export async function handleDownload(
|
|||||||
const headers = new Headers({
|
const headers = new Headers({
|
||||||
"content-type": meta.mimeType || "application/octet-stream",
|
"content-type": meta.mimeType || "application/octet-stream",
|
||||||
"content-length": String(fileStat.size),
|
"content-length": String(fileStat.size),
|
||||||
"content-disposition": contentDisposition(meta.originalName),
|
"content-disposition": contentDisposition(downloadFileName(rawKey, meta.originalName)),
|
||||||
"cache-control": "private, no-store",
|
"cache-control": "private, no-store",
|
||||||
"x-content-type-options": "nosniff",
|
"x-content-type-options": "nosniff",
|
||||||
"referrer-policy": "no-referrer",
|
"referrer-policy": "no-referrer",
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ export function contentDisposition(name: string): string {
|
|||||||
return `attachment; filename="${ascii}"; filename*=UTF-8''${encoded}`;
|
return `attachment; filename="${ascii}"; filename*=UTF-8''${encoded}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function downloadFileName(key: string, originalName: string): string {
|
||||||
|
const dot = originalName.lastIndexOf(".");
|
||||||
|
const ext = dot > 0 && dot < originalName.length - 1 ? originalName.slice(dot + 1) : "";
|
||||||
|
return ext ? `${key}.${ext}` : key;
|
||||||
|
}
|
||||||
|
|
||||||
export function hashPassword(password: string): string {
|
export function hashPassword(password: string): string {
|
||||||
const salt = randomBytes(16).toString("hex");
|
const salt = randomBytes(16).toString("hex");
|
||||||
const hash = scryptSync(password, salt, 64).toString("hex");
|
const hash = scryptSync(password, salt, 64).toString("hex");
|
||||||
|
|||||||
Reference in New Issue
Block a user