From baee5e4d1ba22c491cea310be09133cd131b3267 Mon Sep 17 00:00:00 2001 From: bigben-7 Date: Sat, 17 Oct 2026 11:03:13 +0100 Subject: [PATCH] NteinPrecious: accessible DropZone + strict CSP builder (#1512-#1515) --- frontend/src/components/upload/DropZone.tsx | 71 +++++++++++++++++++++ frontend/src/lib/security-headers.ts | 35 ++++++++++ 2 files changed, 106 insertions(+) create mode 100644 frontend/src/components/upload/DropZone.tsx create mode 100644 frontend/src/lib/security-headers.ts diff --git a/frontend/src/components/upload/DropZone.tsx b/frontend/src/components/upload/DropZone.tsx new file mode 100644 index 0000000..f555b28 --- /dev/null +++ b/frontend/src/components/upload/DropZone.tsx @@ -0,0 +1,71 @@ +/** + * Accessible upload drop zone (#1513, #1512). + * + * The drop zone was not keyboard accessible and its title label was not + * associated with its input. This wires the label to the input via + * `htmlFor`/`id` and supports keyboard activation, using a neutral title + * placeholder rather than the finance example (#1512). + */ +import * as React from "react"; + +export interface DropZoneProps { + id?: string; + label?: string; + accept?: string; + onFile: (file: File) => void; +} + +export function DropZone({ + id = "upload-file", + label = "Choose a document to upload", + accept, + onFile, +}: DropZoneProps): React.ReactElement { + const inputRef = React.useRef(null); + const labelId = `${id}-label`; + + return ( +
+ +
inputRef.current?.click()} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + inputRef.current?.click(); + } + }} + onDrop={(event) => { + event.preventDefault(); + const file = event.dataTransfer.files?.[0]; + if (file) { + onFile(file); + } + }} + onDragOver={(event) => event.preventDefault()} + > + Drag and drop, or press Enter to browse +
+ { + const file = event.target.files?.[0]; + if (file) { + onFile(file); + } + }} + /> +
+ ); +} + +export default DropZone; diff --git a/frontend/src/lib/security-headers.ts b/frontend/src/lib/security-headers.ts new file mode 100644 index 0000000..0525c11 --- /dev/null +++ b/frontend/src/lib/security-headers.ts @@ -0,0 +1,35 @@ +/** + * Content-Security-Policy builder (#1515). + * + * The previous CSP allowed `unsafe-inline`, `unsafe-eval` and connected to any + * `http(s):` origin. This builder produces a strict policy and lets callers add + * only the explicit sources they need. + */ +export interface CspOptions { + connectSources?: string[]; + scriptSources?: string[]; + styleSources?: string[]; + imageSources?: string[]; + allowInlineStyles?: boolean; +} + +export function buildContentSecurityPolicy(options: CspOptions = {}): string { + const directives: Record = { + "default-src": ["'self'"], + "script-src": ["'self'", ...(options.scriptSources ?? [])], + "style-src": [ + "'self'", + ...(options.allowInlineStyles ? ["'unsafe-inline'"] : []), + ...(options.styleSources ?? []), + ], + "img-src": ["'self'", "data:", ...(options.imageSources ?? [])], + "connect-src": ["'self'", ...(options.connectSources ?? [])], + "object-src": ["'none'"], + "base-uri": ["'self'"], + "frame-ancestors": ["'none'"], + }; + + return Object.entries(directives) + .map(([directive, values]) => `${directive} ${values.join(" ")}`) + .join("; "); +}