Rules Hub
Coding Rules Library
Rule priority, scope & exceptions
Use this to align rules with the senior-level structure (P0/P1/P2, scope, exceptions/tradeoffs).
Store uploads outside the webroot and serve them with safe headers
Never place user uploads in an executable/served directory; keep them outside the webroot or in object storage and serve via a handler that forces nosniff, a safe Content-Type, and attachment disposition.
Bad example
| 1 | // uploads land in the statically-served public/ folder |
| 2 | const dest = path.resolve('public/uploads', storedName); |
| 3 | await fs.writeFile(dest, buffer); |
| 4 | // served by Express static / Next.js public: |
| 5 | app.use(express.static('public')); // GET /uploads/<storedName> served verbatim |
Explanation (EN)
Writing uploads into a statically-served directory means they are reachable at a guessable URL with whatever Content-Type the static handler infers from the extension. A stored .html or .svg then executes scripts in your origin (stored XSS), and on misconfigured servers an uploaded script may be executed (RCE). The files also bypass any auth/authorization your app enforces.
Objašnjenje (HR)
Pisanje uploada u staticki posluzeni direktorij znaci da su dostupni na pogodljivom URL-u s Content-Type koji staticki handler zakljuci iz ekstenzije. Spremljeni .html ili .svg tada izvrsava skripte u tvom originu (pohranjeni XSS), a na lose konfiguriranim posluziteljima ucitana skripta moze se izvrsiti (RCE). Datoteke takoder zaobilaze svaku autentikaciju/autorizaciju koju aplikacija namece.
Good example
| 1 | // Stored outside any served directory (or in S3/GCS). |
| 2 | const dest = resolveSafe('/var/app/uploads', safeExt); // see traversal rule |
| 3 | await fs.writeFile(dest, buffer, { flag: 'wx' }); |
| 4 |
|
| 5 | // Served only through an authorized handler with hardened headers. |
| 6 | async function serveUpload(req: Request, res: Response) { |
| 7 | await assertCanRead(req.user, req.params.id); |
| 8 | const { absPath, mime } = await lookupUpload(req.params.id); // mime = verified type |
| 9 | res |
| 10 | .setHeader('Content-Type', mime) |
| 11 | .setHeader('X-Content-Type-Options', 'nosniff') // stop MIME sniffing |
| 12 | .setHeader('Content-Disposition', 'attachment') // download, don't render |
| 13 | .setHeader('Content-Security-Policy', "default-src 'none'; sandbox"); |
| 14 | fs.createReadStream(absPath).pipe(res); |
| 15 | } |
Explanation (EN)
Files live outside the webroot, so they can only be reached through `serveUpload`, which checks authorization and sets the verified Content-Type plus `nosniff`, attachment disposition, and a locked-down CSP. This neutralizes stored-XSS and sniffing attacks. Best of all, serve user content from a separate cookieless domain (e.g. usercontent.example.net) so even a rendered file can't act on your main origin.
Objašnjenje (HR)
Datoteke se nalaze izvan webroota, pa su dostupne samo kroz `serveUpload`, koji provjerava autorizaciju i postavlja provjereni Content-Type plus `nosniff`, attachment disposition i zakljucan CSP. To neutralizira pohranjeni XSS i napade njuskanjem. Najbolje od svega, posluzuj korisnicki sadrzaj s odvojene domene bez kolacica (npr. usercontent.example.net) kako ni renderirana datoteka ne bi mogla djelovati na tvoj glavni origin.
Notes (EN)
Covers OWASP A05 (security misconfiguration) and the File Upload Cheat Sheet's 'serve from a different domain' guidance. The header trio (correct Content-Type + nosniff + Content-Disposition) is the minimum for any user-content handler.
Bilješke (HR)
Pokriva OWASP A05 (sigurnosna pogresna konfiguracija) i smjernicu File Upload Cheat Sheeta 'posluzuj s druge domene'. Trojka zaglavlja (ispravan Content-Type + nosniff + Content-Disposition) je minimum za svaki handler korisnickog sadrzaja.
Exceptions / Tradeoffs (EN)
Object storage (S3/GCS/Azure Blob) satisfies the 'outside webroot' requirement; still set the stored object's Content-Type and disposition explicitly and serve via signed, expiring URLs rather than public-read buckets for non-public content. Public CDN delivery of pre-validated images is fine when the bucket is not your app origin.
Iznimke / Tradeoffi (HR)
Objektna pohrana (S3/GCS/Azure Blob) zadovoljava zahtjev 'izvan webroota'; svejedno eksplicitno postavi Content-Type i disposition spremljenog objekta i posluzuj kroz potpisane URL-ove s istekom umjesto javno-citljivih bucketa za nejavni sadrzaj. Javna CDN isporuka unaprijed validiranih slika je u redu kad bucket nije origin tvoje aplikacije.