Weight the ten controls that stop a malicious upload becoming a web shell and get a coverage score plus the residual risk.
The four controls that decide whether an upload can ever execute — allow-list, content sniffing, storage outside the web root and no-execute permissions — carry 58 of the 100 points between them. Residual risk scales what is missing by who is allowed to upload, so an anonymous form with weak controls lands in the high band. An upload endpoint is the shortest path from anonymous request to code execution on your server, and the controls are cheap — the usual failure is a deny-list of extensions and nothing else.
File Upload Risk
coverage = Σ(weight of each implemented control), weights 16 allow-list, 16 off-webroot, 14 content sniffing, 12 no-execute, 12 malware scan, 8 rename, 8 re-encode, 6 size limits, 5 Content-Disposition, 3 signed URLs = 100.
Residual risk
residual risk = (100 − coverage) × uploader trust ÷ 100, on the OWASP 0–9 scale.
coverage = Σ(weight of each implemented control), weights 16 allow-list, 16 off-webroot, 14 content sniffing, 12 no-execute, 12 malware scan, 8 rename, 8 re-encode, 6 size limits, 5 Content-Disposition, 3 signed URLs = 100. The four controls that decide whether an upload can ever execute — allow-list, content sniffing, storage outside the web root and no-execute permissions — carry 58 of the 100 points between them. Residual risk scales what is missing by who is allowed to upload, so an anonymous form with weak controls lands in the high band.
An upload endpoint is the shortest path from anonymous request to code execution on your server, and the controls are cheap — the usual failure is a deny-list of extensions and nothing else.
This calculator takes 11 inputs: Extension and MIME allow-list (not a deny-list), Content sniffed and matched against the declared type, Stored outside the web root or in object storage, Execution disabled on the storage path, Anti-malware or sandbox scan before release, Server-generated random filename, Images re-encoded and metadata stripped, Size and decompression limits enforced, Content-Disposition attachment with a fixed Content-Type on download, Downloads served through short-lived signed URLs, Who can upload. The pre-filled defaults are a realistic starting point — replace them with figures from your own environment for a result you can act on.
No. Both are attacker-controlled, and a deny-list will miss .phtml, .php5, .cshtml, .jsp variants and double extensions. Combine a positive allow-list with a content-type check on the actual bytes, then make sure the storage location cannot execute anything at all.