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
|
||||
cookie, so access is granted by the server and cannot be forged.
|
||||
- **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
|
||||
stored or emitted in `Content-Disposition`. Path traversal, `../`, encoded
|
||||
traversal, null bytes, and symlink injection via user input are not possible.
|
||||
stored; downloads are sent as `<upload-key>.<extension>`, so the basename in
|
||||
`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.
|
||||
- **MIME / XSS.** Downloads are sent as `Content-Disposition: attachment` (browsers
|
||||
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 { notFound } from "../http";
|
||||
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";
|
||||
|
||||
export async function handleDownload(
|
||||
@@ -41,7 +41,7 @@ export async function handleDownload(
|
||||
const headers = new Headers({
|
||||
"content-type": meta.mimeType || "application/octet-stream",
|
||||
"content-length": String(fileStat.size),
|
||||
"content-disposition": contentDisposition(meta.originalName),
|
||||
"content-disposition": contentDisposition(downloadFileName(rawKey, meta.originalName)),
|
||||
"cache-control": "private, no-store",
|
||||
"x-content-type-options": "nosniff",
|
||||
"referrer-policy": "no-referrer",
|
||||
|
||||
@@ -59,6 +59,12 @@ export function contentDisposition(name: string): string {
|
||||
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 {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync(password, salt, 64).toString("hex");
|
||||
|
||||
Reference in New Issue
Block a user