Detection Engineering

File Upload Security: A Blue Team Checklist

File upload security for blue teams — detect web-shell uploads with FIM and process telemetry, Sigma and YARA rules, plus a hardening checklist.

A file being scanned on a dark workbench with cyan scan lines and a red warning glow

File upload security has one job: stop an attacker from uploading a script and running it as your web server. When validation is weak, an “image” upload becomes a web shell, and the attacker gets persistent command execution through the browser. Detection comes down to two signals — a new executable file appearing in a web-accessible directory, and the web server process spawning a shell. This guide ships the file-integrity and process rules plus the hardening checklist.

Malicious file upload sits under A03:2021 — Injection and the web shell it plants maps to T1505.003 — Server Software Component: Web Shell. The 2025 SAP NetWeaver flaw CVE-2025-31324 — an unrestricted file-upload bug rated CVSS 10.0 and exploited in the wild to drop JSP web shells — is the worked example.

What is a malicious file upload attack?

A malicious file upload attack abuses an upload feature to place an executable script in a location the web server will run. The attacker uploads shell.php disguised as photo.php.jpg (or with a doctored content type), the server stores it in a web-accessible directory, and the attacker requests it in a browser to run commands. The result is the same as command injection: code execution as the web server account, plus persistence.

That persistent script is the web shell, and it is one of the most durable footholds an attacker can get — which is why both prevention (stop the upload) and detection (catch the shell) matter.

What makes a file upload dangerous?

WeaknessWhat the attacker doesTelemetry fingerprintDefense
Extension-only validationUploads shell.php.jpg / shell.phtmlNew script file in an upload dirValidate magic bytes
Executable upload directoryRequests the uploaded script directlyWeb server runs/serves the scriptDisable execution in upload dir
Stored in web rootDirect browser access to the fileGET to an unusual script pathStore outside web root
No content-type checksSends a script with image/jpegMismatch between extension and bytesVerify content, not header

The unifying lesson: the file system and the web-server process are where upload attacks become visible, not the upload request itself, which often looks benign.

How to detect malicious uploads and web shells

Two rules carry most of the weight, backed by file integrity monitoring on every upload directory.

A new executable in a web-accessible upload directory

File integrity monitoring on upload paths is the earliest signal — a script appearing where only media should is almost never legitimate.

Sigma Executable File Created in a Web-Accessible Upload Directory
title: Executable File Created in a Web-Accessible Upload Directory
id: 6e3b1d27-darkpwn-illustrative
status: experimental
logsource:
  category: file_event
detection:
  selection:
    TargetFilename|contains: ['/uploads/','/wp-content/uploads/','/webroot/','/htdocs/']
    TargetFilename|endswith: ['.php','.phtml','.jsp','.jspx','.aspx','.asp','.cfm']
  condition: selection
falsepositives:
  - Legitimate deploys writing scripts (scope to upload dirs, exclude CI/deploy paths)
level: high

The web server spawning a shell (web shell execution)

When the uploaded script runs, the web server process spawns a shell — the same behavioral backbone used for command injection detection.

Sigma Web Server Process Spawning a Shell (Web Shell Execution)
title: Web Server Process Spawning a Shell (Web Shell Execution)
id: 9c4f2a18-darkpwn-illustrative
status: experimental
logsource:
  category: process_creation
detection:
  parent:
    ParentImage|endswith: ['\w3wp.exe','\httpd','\apache2','\nginx','\java','\php-fpm']
  child:
    Image|endswith: ['\cmd.exe','\powershell.exe','\bash','\sh']
  condition: parent and child
falsepositives:
  - CGI apps that legitimately shell out (allowlist the specific pair)
level: high

For content scanning of files at rest, a YARA rule flags common web-shell markers — useful in FIM and IR sweeps, with the caveat that obfuscation evades it.

YARA Common Web Shell Indicators
rule darkpwn_webshell_indicators {
  meta:
    author = "Colson"
    description = "Heuristic indicators of common PHP/JSP web shells"
    attack = "T1505.003"
  strings:
    $a = "eval(" nocase
    $b = "base64_decode(" nocase
    $c = "system(" nocase
    $d = "Runtime.getRuntime().exec" nocase
    $e = /\$_(GET|POST|REQUEST)\s*\[/
  condition:
    2 of them
}

Why does magic-byte validation keep failing?

“Validate by content, not extension” is the correct advice and it is routinely implemented in a way that does not work. The gap is worth understanding precisely, because the fix is different from what most teams write.

Magic-byte checking reads the first few bytes of a file and compares them against a known signature — FF D8 FF for JPEG, 89 50 4E 47 for PNG, 25 50 44 46 for PDF. The check answers one question: does this file begin like a JPEG? It does not answer the question that matters: is this file only a JPEG?

Those differ because most file formats tolerate trailing data. A file can carry a perfectly valid JPEG header, contain a real decodable image, and hold script content further in — inside a comment segment, in metadata, or simply appended after the image data ends. It passes magic-byte validation, opens correctly in an image viewer, and still executes as a script if the server ever interprets it.

BypassWhy validation misses itWhat actually stops it
Polyglot file, valid header plus appended scriptThe header is genuinely correctNever execute uploads; re-encode
Script hidden in EXIF or a comment segmentImage parses fineStrip metadata; re-encode
Double extension (x.php.jpg)Depends entirely on server configRename to a random ID with a fixed extension
Alternate handled extension (.phtml, .phar, .jspx)The allowlist was written for the common casesDeny-by-default; explicit allowlist
Case or trailing-character tricksNaive string comparisonNormalise before comparing
Archive that expands outside its directoryThe archive itself is a valid archiveValidate every extracted path
SVG containing scriptIt is a legitimate SVGServe as an attachment, or sanitise

Two controls collapse most of that table:

  • Re-encode the file rather than validating it. Decode the image and write a new one from the decoded pixels. Anything that was not image data does not survive, without your needing to anticipate the specific trick. The cost is CPU and a small quality loss; the benefit is that the entire polyglot class disappears.
  • Make the storage location incapable of execution. Object storage, a separate host, or a server configuration that will not run scripts in that path. Then a successful bypass produces a stored file rather than a web shell, which is the difference between an anomaly and an incident.

SVG deserves its own note, because it is the format that most often defeats an otherwise careful implementation. It is XML, it can contain <script>, and it is an image — so it lands in image allowlists and then executes in the browser when served inline from your origin. Either sanitise it properly, or serve it with Content-Disposition: attachment so it downloads rather than renders.

What does a web shell do after it lands?

Detecting the upload is the best case. Assuming you will always catch it is not a plan, so it is worth knowing what the second and third signals look like — a web shell that is never executed is harmless, and execution is noisy in ways the upload is not.

  1. Reconnaissance. whoami, id, uname -a, directory listings. The web server process executing any of these is the cleanest possible signal, and it is what the process rule above catches.
  2. Persistence beyond the shell. A cron job, a systemd unit, a scheduled task, an SSH key added to the web user’s authorized_keys, or a second shell dropped in a different directory as a backup. Detecting only the first shell and removing it is the most common incomplete response.
  3. Credential harvesting. Application configuration files are the target — database credentials, API keys, cloud credentials. On a cloud host, the instance metadata endpoint is the highest-value stop, which is the same objective as SSRF and shows up the same way in CloudTrail.
  4. Lateral movement. Outbound connections from a web server to internal hosts it has never contacted. Web servers have extremely predictable egress, which makes this one of the better anomaly detections available on any host in your estate.
  5. Staging and exfiltration. Archives appearing in temp or upload directories, then leaving.

Signals 4 and 5 are worth building even if you trust your upload controls completely, because they catch every route to code execution on a web server rather than this one. A default-deny egress policy converts both from a detection into a prevention, and web servers are among the easiest systems to apply one to — they mostly talk to a database, a cache, and a package mirror.

How do you respond to a confirmed web shell?

  1. Preserve before you delete. Copy the file, record its timestamps, and capture the web logs around its creation. Deleting it first destroys your only record of how it arrived, and the arrival path is the actual vulnerability.
  2. Find the upload in the web logs. The POST that created it identifies the vulnerable endpoint, and its timestamp bounds the investigation window.
  3. Assume there are more. Sweep every web-accessible path for files created since that timestamp, and sweep with a content rule as well as by date. One shell is rarely one shell.
  4. Check what the web user could reach. Its filesystem permissions, its database credentials, and any cloud role attached to the host define the blast radius — and the cloud role is frequently the most privileged thing in that list.
  5. Rotate every secret the host held. Configuration files were readable for the entire period the shell existed. Treat them all as disclosed.
  6. Fix the upload validation before restoring service. Otherwise the same request replays successfully within hours; internet-facing exploitation is automated and continuous.
  7. Rebuild rather than clean where the shell had time to establish persistence. Removing a file is not removing an intrusion.

Step 5 is the one under-scoped most often. The instinct is to treat a web shell as a code-execution incident, and its more durable consequence is usually credential disclosure — the shell can be deleted in a minute, and the database password it read is still valid until somebody changes it.

How to test your upload detection

In a lab app you own:

  1. Upload a benign script to an upload directory and confirm the FIM rule fires.
  2. Execute a harmless test “shell” (one that just runs id) and confirm the web-server-spawns-shell rule fires.
  3. Upload with a doctored extension/content type and confirm validation rejects it.
  4. Replay normal media uploads and confirm the rules stay quiet.

How to prevent malicious file uploads

  • Patch upload-capable applications fast — CVE-2025-31324 was CVSS 10.0 and actively exploited.
  • Run the web server at least privilege so a web shell is boxed in, and confirm the account cannot write to the directories that serve application code — a shell that can only write where nothing executes is a much smaller problem than one that can rewrite the application itself.
  • Default-deny egress from web hosts so a shell’s callbacks are blocked and logged. Web servers have unusually predictable outbound needs — a database, a cache, a package mirror — which makes this one of the few places a default-deny egress policy is genuinely straightforward to write and maintain rather than a multi-quarter programme.

Common file upload detection mistakes

  • WAF-only coverage. The upload request looks normal; the danger is on disk and in the process tree.
  • No FIM on upload directories. The earliest signal goes uncollected.
  • Signature-only scanning. Obfuscation defeats it.
  • Extension allowlists without content checks. shell.phtml and double extensions slip through.
  • Magic-byte validation that only reads the header. A polyglot file has a genuine JPEG header and script content after it. The check passes because the check was answering a different question.
  • Not re-encoding images. Re-encoding removes every appended-payload variant at once, without needing to enumerate the tricks in advance.
  • Allowing SVG into an image allowlist. It is XML, it can contain script, and it renders in the browser from your origin. Sanitise it or serve it as an attachment.
  • Trusting archive contents. A zip entry can contain path traversal in its filename; validate every extracted path before writing it.
  • Deleting the shell before capturing the web logs. The POST that created it identifies the vulnerable endpoint, and that is the only thing worth fixing.
  • Finding one shell and stopping. Sweep everything created since that timestamp; attackers drop backups in unrelated directories precisely because responders remove the first one.
  • Treating it as a code-execution incident only. Every credential in the application’s config files was readable for the shell’s entire lifetime and needs rotating.
  • Restoring service before fixing validation. Internet-facing exploitation is automated; the same request replays within hours.
  • Egress from web servers left open. Web-server egress is unusually predictable, which makes default-deny both achievable and effective against every route to code execution, not just this one.

What a mature upload pipeline looks like

Putting the controls in order, an upload should pass through these stages before it is ever reachable:

  1. Size and type gate at the edge, rejecting the obvious before it consumes resources.
  2. Content validation by decoding, not by header inspection.
  3. Re-encode or transform — images re-encoded from decoded pixels, documents converted or normalised. This is the step that eliminates the polyglot class.
  4. Strip metadata, which removes both hidden payloads and the privacy problem of publishing users’ EXIF GPS coordinates.
  5. Rename to a random identifier with a server-chosen extension, so the client never influences the stored filename.
  6. Store outside the web root — ideally in object storage on a different origin, which makes execution architecturally impossible rather than configuration-dependent.
  7. Serve through a handler that sets Content-Type and Content-Disposition explicitly and never reflects a client-supplied value.
  8. Scan asynchronously and quarantine on a hit, treating the scanner as one signal rather than the gate.

Steps 3 and 6 are the load-bearing ones. Everything else raises the cost of an attack; those two change whether a successful bypass produces a stored file or a running web shell — which is the only distinction that determines whether you have an incident.

File upload security checklist

  1. Validate file type by magic bytes/content, never by extension or client content-type.
  2. Store uploads outside the web root; serve via a handler; rename to random IDs.
  3. Disable script execution in the upload directory at the web-server config level.
  4. Enforce size limits and an allowlist of permitted content types.
  5. Run file integrity monitoring on every web-accessible upload path.
  6. Deploy the new-executable-in-upload-dir Sigma rule and the web-server-spawns-shell rule.
  7. Run the web server at least privilege; default-deny its egress.
  8. Patch upload-capable apps (e.g. CVE-2025-31324) on a KEV cadence.
  9. Fire every rule in a lab against benign and malicious test uploads.
  10. Re-encode images from decoded pixels rather than validating headers, and strip metadata while you are there.
  11. Treat SVG as active content: sanitise it, or serve it with Content-Disposition: attachment.
  12. Validate every path inside an uploaded archive before extraction.
  13. Never reflect a client-supplied Content-Type or filename in the response that serves the file back.
  14. Alert on outbound connections from web servers to destinations they have never contacted.
  15. Preserve the file and the surrounding web logs before deleting anything, and sweep for siblings created in the same window.
  16. Rotate every credential the host’s configuration files contained.

The list is long and it collapses to one architectural question: can a file stored by an upload ever be executed by anything? If the answer is structurally no — because uploads live in object storage on a different origin, are re-encoded on arrival, and are served through a handler that sets its own content type — then items 1, 4, 10, 11, and 12 are defence in depth rather than the thing standing between you and a compromise. If the answer is “not currently, because of a configuration directive,” then that directive is a single edit away from being the whole of your security, and it belongs in a test rather than in someone’s memory.

That test is worth writing explicitly. Upload a benign script to the upload path, request it over HTTP, and assert that the response is either a download or a 403 — never execution. Run it in CI. It is a handful of lines, it directly verifies the control that actually matters, and it fails loudly the day someone changes a web-server config for an unrelated reason.

The takeaway

File upload security is prevention plus two signals: validate uploads by content and store them where they cannot execute, then watch for a script appearing in an upload directory and the web server spawning a shell. Continue the web-application defense arc with SQL injection detection and broken access control testing, or follow malicious content arriving through a trusted path instead of an upload form in supply chain attack detection. Or browse the full Detection Engineering pillar.

Training & tools referenced

Disclosure: Some links below are affiliate links. If you buy through them, darkpwn may earn a commission at no extra cost to you. We only recommend training and tools we actually use in our own lab, and affiliate links never influence editorial coverage.

  • TryHackMeAuthorized labs to practice web-shell detection and upload hardeningSecurity Training
    Start training

Frequently asked questions

How do you detect a malicious file upload?

Watch web-accessible upload directories for newly created executable files (.php, .jsp, .aspx, .phtml) with file integrity monitoring, and watch for the web server process spawning a shell — the signal that an uploaded web shell is being executed. Obfuscation defeats signatures, so behavior and FIM matter more than content matching.

What is a web shell?

A web shell is a malicious script (PHP, JSP, ASPX) an attacker uploads to a web-accessible directory, then accesses through a browser to run operating system commands with the web server's privileges. It maps to MITRE ATT&CK T1505.003 and provides persistent, stealthy access.

How do you prevent malicious file uploads?

Validate file type by content (magic bytes) not extension, store uploads outside the web root and serve them through a handler, rename files to random identifiers, disable script execution in the upload directory, and cap size. The root cause of nearly every upload RCE is weak file-type validation (CWE-434).

Which MITRE ATT&CK technique covers web shells?

Web shells map to T1505.003 (Server Software Component: Web Shell), usually reached through T1190 (Exploit Public-Facing Application) when the upload feature itself is the entry point.