fix(audit): storage cluster — M16 (presign doc/contract), M17 (per-port byte cap), M18 (replay-after-stat), L17 (mime allow-list, fingerprint hash), L22 (brochure portSlug)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:40:56 +02:00
parent 65ed90b603
commit 9305c030de
6 changed files with 132 additions and 26 deletions

View File

@@ -11,6 +11,8 @@
* truth.
*/
import { createHash } from 'node:crypto';
import { and, eq, isNull } from 'drizzle-orm';
import { db } from '@/lib/db';
@@ -47,6 +49,16 @@ export interface PresignOpts {
* slug, so this is the matching enforcement.
*/
portSlug?: string;
/**
* Optional per-port upload byte cap for presigned uploads. Embedded in
* the filesystem proxy token (`b` field) and enforced by the proxy PUT
* handler, so a token minted against a 15 MB-capped port can't be used
* to write a 50 MB object (audit M17). S3 presigned PUTs can't sign a
* content-length-range on this path, so the cap there is re-checked
* server-side at register time. When unset, the proxy falls back to the
* global `MAX_FILE_SIZE` ceiling.
*/
maxBytes?: number;
}
export interface StorageBackend {
@@ -209,7 +221,12 @@ async function loadStorageConfig(): Promise<StorageConfigSnapshot> {
* client is held in memory until the next mismatch.
*/
function fingerprint(cfg: StorageConfigSnapshot): string {
return JSON.stringify(cfg);
// L17(c): hash the serialized config rather than holding the decrypted S3
// access key verbatim in a process-lifetime string. A SHA-256 digest still
// changes whenever any field (including the secret material) rotates, so
// the cache-invalidation semantics are unchanged, but the cleartext secret
// no longer lingers in the cache key.
return createHash('sha256').update(JSON.stringify(cfg)).digest('hex');
}
/**