//nefariousplan

Unauth Write To Execution Path

No auth required to write a file into a path the server executes. Webroot upload, CGI drop, etc.

There is a class of vulnerability where the exploit is a single unauthenticated request that writes a file into a directory the server later executes. Not shellcode. Not memory corruption. Not deserialization cleverness. A file write, into a path the web server is configured to run, by a caller who did not authenticate.

The bug is almost too plain to take seriously until you see how often it lands. It is an architectural shape: the web server has a writable directory, the writable directory is on the execution path, and the write endpoint does not check who is writing. Three decisions, each reasonable in isolation. Together, the three decisions add up to remote code execution for anyone who can reach the HTTP port.

Mechanism

The stack looks like this. A server product exposes a web interface. The web interface has endpoints that accept file uploads: maybe for profile pictures, maybe for configuration, maybe for document import, maybe for certificate management. The uploaded files get stored somewhere on disk. The web server is configured to execute scripts from a directory tree that includes, or can reach, the upload location. An unauthenticated caller uploads a script. The server stores it. The server executes it on the next request to its path.

Each step is documented. Each step is working as designed. The vulnerability is the composition: the write path and the execute path overlap, and the overlap is reachable without credentials.

What makes this specific is the ABSENCE of exotic primitives. No memory safety bugs. No complex parser confusion. No cryptographic weakness. The exploit is a normal HTTP POST. The payload is a normal file. The defender reading the advisory is looking at an attack that could have been prevented by any of: requiring auth on the upload, storing uploads outside the execution root, denying execute on the upload directory, filtering file extensions. All four defenses exist. All four were omitted.

The recurrence is what puts this in a pattern library. New instances appear every quarter in products that have been shipping for decades. The design shape keeps producing CVEs because the shape keeps getting built. Someone chooses to store uploads where the web server runs. Someone forgets that the upload path is reachable without auth. Someone adds a new endpoint that bypasses the auth check in the rest of the app. The result is always the same: one POST to root.

Exhibits

CVE-2026-38360: dash-uploader Shipped a Validation Hook Marked "Important If Your Site Is Public." It Was the Patch.. dash-uploader exposes /API/dash-uploader as an unauthenticated POST route on the host Flask app. The route's handler reads three form fields (upload_id, resumableFilename/flowFilename, resumableIdentifier/flowIdentifier) directly into os.path.join and os.makedirs. The "execution path" is not a webroot here; it is the Python interpreter's site-packages directory, where any file matching *.pth is read by site.py on the next startup and any line beginning with import is exec'd. The pattern's three architectural decisions compose: an unauthenticated trigger, a write that reaches a path the runtime auto-executes, and a contract that does not validate path components. What is distinctive is that the runtime is CPython itself, not a webserver. .pth files are an execution path every Python deployment ships with, and dash-uploader gives any unauthenticated network peer a write into that path.

CVE-2026-7711: MindsDB Built ModelWrapperSafe and ModelWrapperUnsafe. The HTTP Form Picks.. MindsDB's BYOMUpload handler at PUT /api/handlers/byom/<name> is the unauthenticated trigger. The write target is not a webroot; it is tempfile.mkdtemp(prefix="mindsdb_file_"), a fresh directory the developer correctly isolates from user-accessible paths. The "execution path" is the same request: the handler reads the temp file back four function calls later and passes its bytes to Python's exec via a function named import_string. The pattern's three architectural decisions still compose: an unauthenticated trigger (the route is in protected_namespaces but config["auth"]["http_auth_enabled"] defaults to false in the self-hosted build), a write into a path the server later treats as code (the temp file's bytes feed exec(code, module.__dict__)), and a contract that does not validate path contents (check_module runs after the exec). What is distinctive is that the developer also shipped a safer alternative (ModelWrapperSafe, runs in a virtualenv subprocess) and left the choice between the two to the form field type on the upload request. The closest prior exhibit is Nginx-UI, where the execution path was also not a webroot but a config-reload sink the server itself re-read on the next graceful restart.

CVE-2026-27760: OpenCATS' Install Lock Is a File Every Release Ships Without. OpenCATS' install:databaseConnectivity AJAX endpoint is the unauthenticated trigger. The write target is not an uploads directory but config.php itself, the file every entry script includes via include_once('./config.php') on every request. The shape this pattern names is preserved: an unauthenticated POST writes attacker-controlled bytes into a path the server executes. What is distinctive in OpenCATS is that the "execution path" is a single file that already runs on every page load, and the write is a surgical define() line rewrite rather than a new file landing in a directory. The pattern's prior exhibits (Pix for WooCommerce, Breeze Cache, Ninja Forms, SAP NetWeaver) all relied on a write directory that overlapped a server-execution directory; OpenCATS removes the directory entirely by writing into the configuration file itself, and the unauth gate that gets removed is the absence of a sentinel file that the project's own .gitignore and release packaging script ship without.

CVE-2025-6440: WooCommerce Designer Pro Takes the File and the Extension as Separate Fields. WooCommerce Designer Pro's wcdp_save_canvas_design_ajax action is registered with wp_ajax_nopriv_ because the storefront design flow has to accept guest uploads before checkout. The destination is wp-content/uploads/wcdp-uploads/temp/<uniq>/<name>.<ext>, a path inside the WordPress installation that PHP executes by default. Three architectural decisions compose the shape this pattern names: an unauthenticated trigger that is correct for the product's customer experience, a write directory that overlaps the execute path, and a destination-filename contract that does not validate extension. What is distinctive in WCDP is where the destination filename came from. The Pix-for-WooCommerce and Breeze-Cache exhibits took their filename from places a defender reading upload code would think to audit (the multipart $_FILES['name'], an HTTP fetch URL). WCDP took its destination filename from a files[0].ext field three levels deep inside a JSON params document the plugin treats as frontend-to-backend canvas metadata. The plugin invented a parallel filename channel that inherits the backend's trust in its own frontend without inheriting any of the multipart sanitization a normal upload pipeline applies. The exhibit shows that this pattern recurs even when the developer is using a custom upload protocol, because the underlying decision (do not check the extension at the destination) is what matters, not the surface where it was supposed to be checked.

CVE-2026-34413: Xerte's Auth Check Issued the Redirect, Then Did the Upload. Xerte's /editor/elfinder/php/connector.php is an elFinder dispatcher reachable without authentication on a default Guest install. The auth check at the top of the file emits a 302 redirect to the login page when the session is unset, then falls through to the elFinder runner because PHP's header() does not call exit(). The chain composes the pattern's three decisions in one file: an unauthenticated trigger (the gate signals refusal but does not enforce), a write directory (USER-FILES/<n>--Nottingham/) that overlaps the execute path, and an extension blocklist whose php* regex matches ph-prefixed strings rather than php-prefixed strings, leaving .php4 outside the deny list. Three CVEs (34413, 34414, 34415) close in the same one-commit patch; the missing-auth instance is the topmost decision, and the patch closing it is one keystroke (exit(0);) on a line that had been missing since October 2014.

CVE-2025-57819: FreePBX's Ajax Dispatcher Asked PHP If The Class Existed. PHP Loaded The File.. FreePBX's cron_jobs table is the exhibit's execution path: a row inserted via stacked SQLi becomes a shell command run by the cron daemon within the minute, no webroot write required. The unauthenticated dispatcher turns class_exists($module) into an include primitive, and the SQLi sink in the endpoint module turns that include into row-level writes to the executor's queue. The pattern's shape — find any table the system already polls and treats as code — fits cleanly: cron_jobs is the queue, the daemon is the loader, the SQLi is the pen.

CVE-2026-21877: The Helper That Stops This Bug Was Already in n8n. n8n's Git node is a write primitive into a user-supplied path. Pre-patch, no validation existed on repositoryPath. The directory ~/.n8n/nodes/ is on n8n's execute path: the community-node loader scans it and require()s every package's main file at module-load time, executing top-level code before any workflow runs. The composition is the same shape this pattern names. The destination for a git clone overlapped with a directory the server later evaluated as code, and a write endpoint reached the overlap. The access bar is "authenticated workflow author" rather than "anonymous HTTP," so this is the auth-required cousin of the unauth instances at Breeze Cache, Pix for WooCommerce, and SAP NetWeaver. The substrate is unchanged. What is distinctive here is that the helper that would have blocked the write was already in the codebase, was already wrapping every other file-writing node's filesystem operations, and was simply not called from the one node whose entire purpose is writing to wherever the caller asks.

CVE-2026-0740: Ninja Forms Shipped the Patch on February 10. The Bug Shipped Until March 19.. Ninja Forms File Uploads is a public-by-design contact-form extension whose nf_fu_upload AJAX handler writes attacker-controlled bytes to wp-content/uploads/ninja-forms/tmp/, a directory inside the WordPress installation served by the PHP interpreter. The unauthenticated trigger is intended; the form is supposed to accept submissions from anonymous visitors. The architectural failure is the overlap between the write directory and the execute path, combined with an upload contract that validated only the source filename while the operation used a separate destination filename the attacker controlled. Same composition this pattern names: unauth + write + execute, three reasonable decisions in isolation, RCE in combination. The Ninja Forms case is distinctive in that the vendor saw it, patched it on February 10, and shipped a version that still allowed the write for another 37 days.

CVE-2026-3844: The Gravatar Fetcher Fetched Anything. Breeze Cache writes its gravatar cache to wp-content/cache/breeze-extra/gravatars/, a directory the WordPress PHP handler executes from. The write trigger is an unauthenticated POST /wp-comments-post.php whose comment author contains a literal srcset= substring. The render filter that processes the comment's avatar HTML downloads the URL the regex extracts and writes the bytes under the attacker-supplied filename. Three architectural decisions composed: an unauthenticated trigger, an overlap between write directory and execute path, and a fetch contract that did not validate file identity. The Breeze instance is the same shape this pattern names with a different filename and a 400,000-install blast radius.

CrushFTP CVE-2025-31161: MFT Is the Target Now. Managed file transfer is, architecturally, a product whose entire purpose is accepting files from the outside. CrushFTP's pre-auth CVE reduced to a request path that wrote an uploaded file into a directory the admin interface later read as code. The MFT product category has spent the last three years establishing that it is structurally the target: MOVEit, Cleo, GoAnywhere, CrushFTP. Each advisory is a new instance of the same shape. The write endpoints and the execution paths are close together because the product's business is moving files, and the business keeps expecting the product's shape to be different this time.

CVE-2026-1306: midi-Synth's Empty masterAPIkey Is the Payload. The handler's write target is plugin_dir_path(__FILE__) . "sound/" . $_fileName — wp-content/plugins/midi-synth/sound/, served as PHP by the webserver. sanitize_file_name does not block the .php extension. An unauthenticated POST writes a PHP file into the webroot, browse-able and executable as PHP. The post composes this pattern with itself: the file the privileged handler writes is itself a multipart upload form, so a second unauthenticated POST against the dropped PHP writes another file into the same webroot directory. Two layers of unauth write into an execution path stacked, the privileged write installing a more permissive primitive.

CVE-2026-20253: Splunk Hardcoded `-h localhost`. The `database` Field Is A libpq Connection String.. CVE-2026-20253 is the Splunk-app-loader instance of the pattern. The unauthenticated trigger is POST /splunkd/__raw/v1/postgres/recovery/{backup,restore}, reaching the sidecar's pg_dump / pg_restore invocation before any auth check until the 10.0.7 / 10.2.4 patches. The write primitive is PostgreSQL's lo_export, smuggled through a conninfo override that lets the attacker first redirect pg_dump to their own Postgres (hostaddr=attacker.db) and then redirect pg_restore to the local Postgres using the sidecar's bundled .pgpass (passfile=/opt/splunk/var/packages/data/postgres/.pgpass). The destination is any path the splunk user can write; the demonstrated target is /opt/splunk/etc/apps/splunk_secure_gateway/bin/ssg_enable_modular_input.py, a file the Splunk app loader treats as executable code on the next modular-input fire. The auto-execution path is not a webroot. It is Splunk's app-loader convention, the same convention every installed Splunk app inherits; the n8n Git and Nginx-UI exhibits sit alongside it as the non-webroot variants where the execute surface is the runtime's own configuration or extension reload, not the HTTP-served filesystem.

CVE-2026-3891: The Capability Check Is Missing Because the Nonce Check Was Never a Capability Check. The Pix for WooCommerce C6 handler wrote attacker-controlled files to wp-content/plugins/payment-gateway-pix-for-woocommerce/Includes/files/certs_c6/, a directory inside the webroot served by the PHP interpreter with no execution restriction. The handler called sanitize_file_name on the uploaded filename but performed no extension check, so shell.php wrote as shell.php. The missing authentication was the nonce-is-not-auth failure; the missing upload restriction is what made authentication failure consequential. Two unauthenticated POST requests from first contact to PHP execution, both composing into the unauth-write-to-execution-path shape this pattern names.

SAP NetWeaver CVE-2025-31324: When CVSS 10.0 Means What It Says. An unauthenticated endpoint in NetWeaver that accepts a file upload, stores it under the web application root, and serves it on subsequent requests. The CVSS 10.0 is not hype. The exploit is a curl command. The remediation is a patch that belatedly adds auth to the endpoint. The architectural mistake is that the endpoint existed as a write path reachable without auth into a directory that the web server is configured to execute from.

CVE-2026-42238: Nginx-UI's Backup Signature Is Signed By Whoever Sends the Backup. The "execution path" here is not a webroot. It is cosysettings.ConfPath, the INI file the next process re-reads on graceful restart. The restore handler copies attacker-controlled app.ini to that path and calls risefront.Restart() two seconds later; the new process loads [nginx].TestConfigCmd, ReloadCmd, RestartCmd, and [terminal].StartCmd into in-memory settings, and each is read by code that hands the string verbatim to /bin/sh -c (internal/nginx/exec.go) or exec.Command (internal/pty/pipeline.go). The [node].Secret field in the same file is the master auth bypass, so the same write that planted the shell sinks also minted the credential needed to invoke them. Three architectural decisions composed: an unauthenticated trigger (the time-gated restore route), a write directory that overlaps the execute path (config fields piped to a shell on the next config-touching action), and a contract that did not validate which fields a "config" file is allowed to set.

CVE-2026-5718: The Drag-and-Drop CF7 Nonce Mint Blocks One Caller, And Its Name Is curl. The vulnerability ends at move_uploaded_file into /wp-content/uploads/wpcf7-files/2026/05/, a directory served by the same Apache that routes \.php$ to mod_php. There is no execution gate between the unauthenticated POST and the resulting shell.php URL. The bypass shapes change each release; the write-to-execute path is the constant.

Boundaries

Not every file upload bug. If the upload endpoint requires authentication, it is still a bug (post-auth RCE), but it is a different pattern. Unauth-write-to-execution-path specifically requires that the caller did not have to authenticate. The missing auth is the essential feature. Add auth, and the bug becomes an insider-threat vulnerability, which belongs in a different conversation.

Not every webshell upload. Once an attacker has any code execution or any authenticated write into an exec-path directory, they can upload a webshell. That is a post-exploitation step. Unauth-write-to-execution-path describes the initial access vector: the exploit that got them to the first execution. Conflating the two makes the pattern look like it is everywhere, which flattens the specific architectural signal.

Not general deserialization. Deserialization RCEs work by smuggling attacker-controlled object graphs through serialization boundaries. Those attacks require specific gadget chains and specific library versions. Unauth-write-to-execution-path works with a plain file. The distinction matters for detection: deserialization attacks have characteristic traffic shapes; unauth-write-to-execution-path usually looks exactly like a normal file upload until the stored file runs.

Defender playbook

For every web-facing service in your fleet, enumerate the intersection of its upload paths and its execute paths. If the two touch, that is your exposure. The audit can be mechanical: list the directories the web server is configured to execute from, list the directories that receive user-uploaded content, compute the intersection. A non-empty intersection is the pattern.

Check which upload endpoints are reachable without authentication. The answer is never zero. Every product has a login page, an account recovery flow, a support channel, a health check. Some of those accept POST bodies. Some of those write those bodies somewhere. Map the actual HTTP surface, not the documented one.

Disable execution on upload directories, structurally, not by configuration that can be undone in the next deploy. If the directory is on a filesystem mounted noexec, no subsequent config drift will re-enable it. Defense-in-depth here means making the simple mistake impossible to make again.

Treat file extensions as untrustworthy input. The server should not decide how to execute a file based on the file's extension. That assumption produced the pattern's first generation. Modern exploits layer on it: polyglot files, extension confusion through URL encoding, server-specific path parsing quirks. The correct answer is not better extension filtering. The correct answer is not executing from the write path at all.

Log every unauthenticated write. If your logs cannot tell you which endpoints accepted writes from unauthenticated callers in the last hour, you are operating blind on the attack surface this pattern needs. The log question is not "who logged in," it is "who wrote a file without logging in."

Kinship

Design Debt Driver. Products that produce unauth-write-to-execution-path CVEs repeatedly are design-debt-drivers in this specific shape. MFT, SAP, legacy web applications, anything with "shared filesystem between upload and execute" as an architectural assumption. The instances differ. The substrate is the same.

Disclosure After Exploitation. Unauth-write-to-execution-path CVEs in enterprise products are frequently first known through in-wild exploitation. The attackers scanning for these products find them before the researchers do, because the exploits are cheap to weaponize and the products are directly internet-exposed. The CVE lags the exploitation.

Trust Inversion. A successful exploit of this pattern converts an unauthenticated network peer into code running inside the server's trust boundary. The server's trust in its own execution path becomes the attacker's foothold. From there, the attacker inherits everything the server could do.

One POST to root. The attack is as old as CGI and as young as the latest MFT advisory.