Executive Summary
QNAP NAS devices suffer from a recurring class of architectural security failures that go well beyond individual CVEs. Researchers at Runic Labs analyzed four vulnerabilities spanning two plugins—Notes Station 3 and QmailAgent—and found that each one traces back to the same platform-level decisions: header values flowing into shell commands, hand-rolled SQL escaping against unquoted fields, and container isolation that maps the container root directly to the host root. The result is that a single pre-auth HTTP request can chain through a shell-exec injection, a world-writable crontab, and an unremapped user namespace to deliver host root in three commands.
CVE-2026-34007 and CVE-2026-34008 were reported to QNAP after ZDI declined them ahead of Pwn2Own. QNAP patched both and issued CVEs but withheld the bounty. The more important takeaway is structural: QTS optimizes for plugin velocity rather than confinement. Without a platform-wide authentication layer, mandatory parameterized queries, structured IPC, and real container scoping, the same vulnerability classes will keep appearing in each new plugin that ships.
Backstory
The research began around Pwn2Own season. A vulnerability chain targeting QNAP NAS systems was assembled and submitted to ZDI, which declined to move forward with it. Rather than sitting on the bugs, the researcher reported them directly to QNAP. QNAP patched the issues and assigned the two CVEs—CVE-2026-34007 and CVE-2026-34008—but did not pay the bounty. The post exists in part to document what was found and in part to make the argument that these are not accidents but symptoms of how the platform is built.
The Setup
Four vulnerabilities across two plugins illustrate the same underlying pattern. The platform consistently chooses the unsafe default and requires developers to manually opt into safety.
| Plugin | Class of Bug | Underlying Cause |
|---|---|---|
| Notes Station 3 | Pre-auth RCE | Header into string-concat shell command into shell_exec |
| Notes Station 3 | Local priv-esc | World-writable crontab consumed by root monitor |
| Notes Station 3 | Container escape | Writable host home-dir mount, no userns remapping |
| QmailAgent | Pre-auth SQL injection | Hand-rolled escape() against an unquoted integer field |
Threat Model
QTS presents a wide attack surface: web ports (80, 443, 8080, 8081), SSH (22), SMB (139/445), NFS (2049), and additional plugin-specific endpoints. The core security problem is not weakness within individual zones but failure at the boundaries between them. Four trust boundaries are consistently violated: Internet to QTS web (where only authLogin.cgi performs real authentication); QTS web to plugin container (where untrusted headers pass through unchanged); container to host (over localhost HTTP IPC, via bind-mounted paths, and through container-root-to-host-root identity mapping); and plugin to plugin (through shared databases, shared sockets, and untracked dependency relationships).

How a Request Becomes a Shell Command
QTS serves HTTP through ELF binaries, passing context via environment variables. Cross-service communication within the platform goes over localhost HTTP, but plugins have no structured IPC library available. The path of least resistance—and the one the codebase already demonstrates—is to shell out to curl for any call that needs to reach another service. When an attacker-controlled header such as X-Forwarded-For flows into that shell-out via string concatenation without escaping, the result is pre-auth remote code execution.
The send_security_mail endpoint in Notes Station 3 is the concrete example. The structural problem is not that one developer made a mistake: it is that calling another service across the container boundary is a shell command in the first place. Any value that reaches that call becomes part of a shell command, and the platform provides no mechanism to prevent it.

shell_exec. Source: original article.Plugins Are Their Own Worlds
Notes Station 3 is built on Laravel. QmailAgent is built on Roundcube. Each plugin implements its own authentication, its own session handling, and its own input validation—independently, without coordination with the platform. The result is that what counts as “pre-auth” varies by plugin interpretation rather than by any shared platform definition. Session primitives (NAS_SID, QTS_SSID, plugin-specific cookies) coexist without any standardized validation layer. An endpoint that one plugin developer considers protected may be reachable without authentication depending on how the plugin handles token validation.
The Duct Tape Layer Around Open Source
QNAP wraps upstream open-source projects with bridge code that creates new attack surface at each seam. Identity is ferried between authentication systems through bridge tables. Database escaping is hand-rolled instead of delegated to parameterized queries. Pre-auth decisions are made outside the upstream framework’s security layer. IPC shims—like the vulnerable curl wrapper—are bolted on without the safety guarantees the original framework was designed to provide. The pattern repeats: take good open-source software, add integration code that bypasses the security-relevant parts, and ship it.
QmailAgent’s SQL injection illustrates this directly. Roundcube uses PDO and prepared statements throughout its own codebase. The QNAP bridge code that routes authentication through a QTS user lookup uses a hand-rolled escape() function against an unquoted integer field—a class of mistake that parameterized queries make structurally impossible.
Apps Depending on Apps
QPKG manifests record inter-plugin dependencies, but the platform does not model the security implications of that dependency graph. A vulnerability in a shared dependency propagates to every plugin that depends on it. A patch to one plugin can silently break assumptions in another. When a plugin is uninstalled, any trust relationships it established—shared database tables, shared sockets, shared mounts—are not revoked. The attack surface grows as the dependency graph grows, and there is no platform-level accounting for it.
It’s in a Container
CVE-2026-34008 is the container escape. Starting from a www-data foothold inside the Notes Station 3 container, host root access is reachable in three commands. The crontab that the root monitor on the host polls is world-writable inside the container. The /share tree—including every user’s home directory and therefore .ssh/authorized_keys—is bind-mounted with write access. And the container runs without user-namespace remapping: the identity map 0 0 4294967295 means container root is host root.
None of these properties are accidents. They reflect deliberate platform decisions. Fixing any one of them independently would raise the cost of exploitation. Fixing all three would make this class of escape structurally unavailable.

Obscurity Is Not a Boundary
authLogin.cgi encrypts its .text segment, which creates the appearance of a protected binary. Symbol tables remain plaintext. Compiled plugins written in Go or packaged with PyInstaller retain enough metadata for reconstruction. Internal localhost services are labeled “undocumented” but are trivially reachable from any plugin that has achieved code execution. Security through obscurity in this context means the platform author believes the barrier is higher than it is for an attacker who has already cleared the first fence.
Undoing It
The encrypted .text segment can be recovered from memory at runtime. The binary decrypts itself on load; dlopen() forces that decryption, and /proc/self/maps shows exactly where the decrypted pages live. The following utility dumps the executable region:
// dump_authlogin.c
// gcc -O0 -o dump dump_authlogin.c -ldl
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
if (argc < 3) { fprintf(stderr, "usage: %s <authLogin.cgi> <out.bin>\n", argv[0]); return 1; }
if (!dlopen(argv[1], RTLD_NOW | RTLD_LOCAL)) {
fprintf(stderr, "dlopen: %s\n", dlerror());
return 1;
}
FILE *m = fopen("/proc/self/maps", "r");
char line[512];
while (fgets(line, sizeof line, m)) {
unsigned long lo, hi;
char perms[8], path[256] = "";
sscanf(line, "%lx-%lx %7s %*x %*s %*d %255s",
&lo, &hi, perms, path);
if (strstr(path, "authLogin.cgi") && perms[2] == 'x') {
FILE *o = fopen(argv[2], "wb");
fwrite((void*)lo, 1, hi - lo, o);
fclose(o);
fprintf(stderr, "dumped %lx-%lx (%lu bytes)\n",
lo, hi, hi - lo);
break;
}
}
fclose(m);
return 0;
}
./dump /home/httpd/cgi-bin/authLogin.cgi authLogin.text.bin
What Would Actually Change This
The recommended fixes are architectural, not patch-level. Parameterized queries in place of hand-rolled escape(). Structured RPC APIs in place of shell_exec wrappers around curl. A platform-wide authentication layer that plugins call rather than reimplement. Container scoping with read-only mounts where write access is not required, and mandatory user-namespace remapping so that container root does not equal host root. A pre-auth surface inventory that the platform tracks and enforces rather than leaving to individual plugin developers to discover.
Each of these is achievable independently. None requires redesigning QTS from scratch. What they require is accepting that plugin velocity is less important than confining the blast radius of any single mistake—a trade-off the platform has not yet made.
Closing
QTS optimizes for getting plugins onto the device quickly. The platform makes the unsafe option the default and the safe option a manual opt-in at every layer: IPC, database access, authentication, container isolation. A single architectural framework covering all four would not eliminate every vulnerability, but it would make entire classes of bugs structurally unavailable rather than merely discouraged.
One development trend makes this more urgent: AI coding agents are now visible in QNAP plugin source trees. Those agents reproduce the string-concatenation patterns that already exist in the surrounding code. A codebase that teaches by example teaches the worst patterns at scale.
Key Takeaways
- CVE-2026-34007 (pre-auth RCE via header injection into
shell_exec) and CVE-2026-34008 (container escape to host root) both trace to platform-level architectural choices, not one-off developer mistakes. - QTS passes untrusted headers through the web-to-container boundary without sanitization, enabling attacker-controlled values to reach shell commands in downstream plugins.
- Container isolation is nominal: no user-namespace remapping means container root equals host root, and world-writable host-polled crontabs are accessible from within the container.
- Each plugin reimplements authentication, session handling, and input validation independently. The inconsistency is the vulnerability surface.
- QNAP wraps upstream open-source frameworks (Laravel, Roundcube) with bridge code that bypasses the security-relevant layers of those frameworks.
- The obscured
authLogin.cgi.textsegment is recoverable from memory at runtime in under 20 lines of C. - AI coding agents in plugin source trees reproduce the unsafe patterns already present in the codebase, amplifying the risk at scale.
Defensive Recommendations
- Mandate parameterized queries platform-wide. Remove hand-rolled escaping functions. PDO prepared statements are available in the PHP stack; use them everywhere.
- Replace
shell_execIPC with structured RPC. Expose internal services through typed function calls or a local message bus. Attacker-influenced values must not reach a shell command. - Enable user-namespace remapping for all plugin containers. Container UID 0 must not map to host UID 0. This single change eliminates an entire class of container escape.
- Audit and restrict bind mounts. Plugin containers should not have write access to host home directories, host crontabs, or any path the host root monitor reads. Default to read-only.
- Centralize authentication. Define a single platform API for pre-auth vs. post-auth decisions. Plugin-local reimplementations should not be permitted to override it.
- Maintain a pre-auth surface inventory. Every endpoint exposed without authentication should be registered at the platform level and reviewed on each plugin release.
- Restrict AI coding agent use in plugin development to security-reviewed templates. Agents that learn from the existing codebase will reproduce existing vulnerabilities at scale.
Conclusion
The QNAP vulnerability pattern is architectural. Four bugs across two plugins share the same root causes because those causes are baked into the platform. Patching individual CVEs closes specific holes; it does not change the conditions that produce them. Structured IPC, mandatory parameterized queries, a centralized authentication layer, and real container scoping with user-namespace remapping would each independently raise the cost of exploitation. Together they would make the recurring vulnerability classes structurally unavailable. The decision to prioritize plugin velocity over blast-radius confinement is still being made with every new plugin that ships under the current platform model.
Original text: “The QNAP Pattern” by Runic Labs.

