//nefariousplan

Content Is Command

External content feeds an interpreter that treats it as instructions. Prompt injection is one instance.

There is a line in every system between what is content and what is command. Content gets stored, displayed, parsed, analyzed. Command gets executed, trusted, acted upon. The security of the system depends on the line holding.

Content-is-command is what happens when the line does not hold. An input channel the system treated as passive data turns out to be the same channel it reads instructions from. The attacker's job is not to bypass a check. The attacker's job is to write content that the interpreter reads as command, and then let the interpreter do the rest.

Mechanism

The pattern shows up wherever an interpreter reads a stream that mixes sources. A shell executes what its user types. A template engine substitutes variables and also evaluates expressions. A SQL query combines structure and values. A PDF renderer follows document instructions and user preferences. An LLM reads its prompt and also any text that happens to be in the prompt.

Each of these systems assumes a boundary: the command part is trusted, the content part is data. Attacks on this pattern do not break the interpreter. They exploit the ambiguity at the boundary. The attacker embeds instructions inside content the interpreter was supposed to treat as inert, and the interpreter, doing exactly what its spec says, does the wrong thing.

What distinguishes content-is-command from ordinary injection is the CONTENT source. Classical injection comes from an input channel the designer already knew was hostile: HTTP parameters, form fields, URL query strings. The defense is input validation at the boundary. Content-is-command comes from channels the designer treated as content: documents, emails, webpages, PDFs, support tickets, database fields populated long ago by someone else. The defense is harder because there is no clean boundary to validate. The content and the command arrive on the same wire, in the same format, routed by the same parser.

Exhibits

CVE-2026-44339: PraisonAI's Permission Tier Was Never the Gate. PraisonAI's ToolExecutionMixin.execute_tool is content-is-command staged across two interpreters with the agent runtime in between. The model is the first interpreter; it reads attacker-shapeable prompt content (user messages, retrieved documents, web results, file contents, prior tool outputs) and emits a structured tool_calls response. The agent's resolver is the second interpreter; until v4.6.37 shipped, it took the model's function.name string and walked globals() and __main__ for any callable matching that name. The pattern's mechanism reads "the interpreter, doing exactly what its spec says, does the wrong thing"; here the wrong thing is two interpreters doing the right thing on either side of a resolution call. The closest sibling exhibit is adx-mcp-server (post 188), which stages the same shape with KQL in the argument slot rather than the function-name slot. PraisonAI is what happens when the payload moves from an argument value into the name field of the tool call itself: the framework's documented permission gate (_perm_allow) defaults to None, so the function-name slot is enforced only by what the resolver refuses to look up.

CVE-2026-4257: Supsystic Sanitizes Prefill For HTML, Renders It Through Twig_Loader_String. Supsystic's prefill is content-is-command at its smallest installable form: a WordPress plugin whose contact-form pipeline stores attacker-influenced URL parameters in a field-value attribute, concatenates that attribute into the form's HTML string, and passes the whole string to Twig_Environment::render() as template source. The content channel is $_GET[<field_name>]. The interpreter is Twig 1.16.0 with Twig_Loader_String and no SandboxExtension. The bridge is the absence of a brace-escape between sanitize_text_field() (named for HTML-attribute context) and the renderer (which reads {{ ... }} regardless of HTML context). The closest sibling exhibit on the catalog is Tandoor Recipes (CVE-2025-23211), where bleach ran in front of Jinja2. This exhibit adds a teaching specific to WordPress's sanitization vocabulary: sanitize_text_field, esc_attr, esc_html, esc_url, esc_js are each sorted by output context, and reaching for one of them is proof the developer understood the threat existed but routed the input to a sink the vocabulary does not name. The 1.8.0 patch's str_replace(['{','}'], ['&#123;','&#125;'], ...) is the in-line escape Tandoor would have applied if its maintainers had decided to keep Jinja2 instead of removing it. The renderer remains, the other content feeders into it remain, and the next CVE is the next user-controllable string concatenated into the form HTML that the brace-escape doesn't cover.

CVE-2025-68664: LangChain's Dumper Forwarded `lc`. Its Loader Trusted It.. CVE-2025-68664 is content-is-command at the boundary between two halves of one library. The "content channel" is any LangChain-serialized blob: a chat history record, a tracer output, a cached structured output, an on-disk vector store. The "interpreter" is the Reviver hooked to json.loads. The bridge is the absent producer-side escape: dumps() forwarded user dicts containing the reserved key lc straight to JSON, and loads() then dispatched on lc == 1 as a framework-object signal. The pattern's mechanism reads "the interpreter, doing exactly what its spec says, does the wrong thing"; the Reviver's secret-typed branch did exactly what it said: looked up os.environ[id[0]] and substituted the value. The widening teeth move is the framework calling its own warned-against load() on its own state at seven internal callsites (runnables/history.py x4, tracers/log_stream.py x2, vectorstores/in_memory.py x1), every one of which the patch had to amend with allowed_objects in the same diff that finally taught dumps() to escape user lc dicts. Closest sibling is Langflow CVE-2026-33017 (post 74): operators treat the flow JSON as content, the server treats template.code.value as command via exec(). Same producer-consumer asymmetry, different reserved field.

CVE-2025-59528: Flowise's convertToValidJSONString Is eval. The Patch Is JSON5.. Flowise's Custom MCP node accepts a config string the user types into the canvas. The string is canonically not strict JSON because the field is a code editor that allows JS-style permissive object literals, so a function called convertToValidJSONString exists to lift it into a form JSON.parse will accept. The lift uses Function('return ' + inputString)(). The content channel is the user's typed config; the interpreter is the JavaScript runtime; the bridge is a function whose name announces that the input is in a different grammar than the JSON parser at the bottom of the call. The fix replaces Function() with JSON5.parse, which is the simplest instance of the pattern's mechanism made visible: the bug was not the input, it was the parser choice, and the parser choice was the wrong interpreter for the content. Sibling exhibits (protobufjs, graphql-ruby, Handlebars) at least had the cover story of dynamic method generation at definition time. Flowise had no codegen at all. The Function() call existed only to relax a JSON spec, and the relaxation was the entire vulnerability.

CVE-2026-35273: PeopleSoft's Environment Hub Deserializes Anyone Because It Was Built to Deserialize Its Agents. CVE-2026-35273 is content-is-command in its oldest and most literal form: Java native deserialization. The PSEMHUB hub reads the OPERATION form parameter, base64-decodes it, and passes the bytes to ObjectInputStream.readObject(), an interpreter whose entire job is to treat serialized content as instructions for which classes to instantiate and which methods to invoke during construction. The nuclei detection proves it with a URLDNS gadget (DNS callback on the inbound request); the weaponized chain swaps in a code-execution gadget and reaches a second interpreter, XMLDecoder, on the next restart. Where the GlassFish exhibit fed attacker XML to an EL evaluator, this feeds an attacker object graph to the serialization runtime. The spec says OPERATION is a peer message; the runtime treats the message as commands, because that is what deserialization is.

CVE-2026-40517: fN Was the One r2 Command That Skipped the base64 Envelope. radare2's RAD mode is content-is-command stated as design intent. The PDB parser's job in print_gvars is to convert binary file content into r2 commands; the idp command's first action is r_core_cmd0(core, ".idpi*"), which feeds those generated commands back into the same interpreter that handles user input. The bridge between content and command is the cb_printf call sequence inside print_gvars. The bug is the textbook instance of the pattern: an attacker-controlled symbol name shipped through cb_printf as the realname operand of fN, with no encoding envelope around it. The codebase had already named the boundary; base64: is the prefix every other producer in libr/anal and libr/core uses to ship user content through a command line, with matching decoders in cmd_flag, cmd_log, cmd_mount, cmd_open, cmd_write, cmd_anal, socket/run, main/radare2, and cmd. CCu and agn had been hardened years before the CVE landed. fN had not.

CVE-2026-41242: protobufjs's Type Name Was JavaScript. protobufjs's Root.fromJSON reads a descriptor (content) and calls Type constructors that store the dictionary keys as type names. Five files later, those names cross into @protobufjs/codegen, which builds source by string concatenation and finalizes with Function(source)(). The name-slot is the parameter without an escape formatter; every other format specifier in the codegen library (%i, %j, %s) carries an escaping convention precisely because every other slot was always understood to be a content-side input. The function-name slot was understood to be source-side and never named as such. The pattern's mechanism reads "the interpreter, doing exactly what its spec says, does the wrong thing"; codegen's spec was a one-line README and an implicit identifier requirement. The closest sibling exhibit is graphql-ruby CVE-2025-27407, where Schema::Loader.load interpolated JSON-supplied names into a class_eval HEREDOC. The shapes match line for line: JSON content, name-field interpolation, codegen sink, eval-class final step, regex-validator fix.

CVE-2025-27407: graphql-ruby Loaded the Schema by Compiling It. graphql-ruby's Schema::Loader.load is content-is-command at a layer below the application. The content channel is a JSON introspection document describing types; the interpreter is the Ruby compiler; the bridge is class_eval <<-RUBY ... def #{method_name}; self[#{method_name.inspect}]; end RUBY, where method_name is whatever string came out of the JSON's name field. The loader's documented purpose is to ingest external schemas, so the content source is wide by design. The DSL the loader feeds was written for author-typed identifiers, so the interpreter at the bottom of the call stack does not distinguish caller-source from wire-source. The PoC's payload is a four-line method-definition wedged into a name field; once the HEREDOC interpolates it, system(...) runs at class-definition time. The fix removes the eval (define_method with a captured closure) and validates names against /^[_a-zA-Z][_a-zA-Z0-9]*$/, naming both halves of the boundary the original code did not.

CVE-2025-23211: bleach Is An HTML Sanitizer. Jinja2 Does Not Read HTML.. The Tandoor case is content-is-command reduced to its smallest reproducible form: a single text column in a database, compiled every time the record is read, by an interpreter that was never given a sandbox. The pattern's mechanism_md says "a template engine substitutes variables and also evaluates expressions"; Tandoor's pipeline is that sentence turned into Python. The exhibit adds a specific teaching about adjacency: a sanitizer for one grammar running in front of an interpreter for another is not a layered defense. It is two unrelated checks at the same call site. The sanitizer sees HTML; the interpreter sees Python. They share a variable name and nothing else.

CVE-2026-40261: The Injection Is in syncCodeBase, Not generateP4Command. Repository configuration values in composer.json and package source metadata from a Packagist-compatible registry are treated as passive connection parameters: port, user, depot, branch reference. The Perforce driver interpolates them directly into shell command strings. A developer who never authored a shell command has, through their dependency declarations, provided input to a shell interpreter. The registry delivery path makes the content-is-command shape explicit: attacker-controlled metadata from a registry becomes a shell command on the developer's machine without any file the developer explicitly ran.

Prompt Injection Is a Supply Chain Attack. An LLM reads its prompt. The prompt contains the user's question, the system's instructions, and any other content the integration has assembled: document excerpts, email bodies, retrieval-augmented context. To the model, these are all text in the same context window. An attacker who controls a piece of text that reaches the prompt reaches the instruction stream. The agent doing the summarization reads the attacker's words as a directive, then calls tools with the attacker's intent. The attack is not a bug in the model. It is the model doing its job against an input whose provenance the prompt could not encode.

CVE-2026-27760: OpenCATS' Install Lock Is a File Every Release Ships Without. The content channel is four HTTP POST parameters (user, pass, host, name) the installer expects to be database connection strings. The interpreter is PHP itself, reading config.php on every request through include_once. The bridge is "'" . $_REQUEST['user'] . "'", a four-character escape envelope that assumes the value belongs in single quotes and emits source code that delimits it that way. The pattern's mechanism reads "the interpreter, doing exactly what its spec says, does the wrong thing"; PHP, given a config.php whose define() line has been rewritten to define('DATABASE_USER', 'admin'); system($_GET['c']); //');, parses it as one define call, one system call, and one comment. The 2026 patch replaces the string-concatenation bridge with var_export, the PHP standard library's content-to-source escape function, which had been in the language since April 2002.

CVE-2026-33980: adx-mcp-server's Three "Safe" Tools All Call execute. The Approval Policy Was the Tool's Name.. CVE-2026-33980 stages content-is-command across two interpreters with the MCP server in between. The model is the first interpreter: it reads attacker text from a document, an email, a row in the same ADX cluster it has been told to summarize. Once convinced, it emits a tool call with table_name carrying KQL. The MCP client, gating on tool name rather than tool body, auto-approves get_table_schema and forwards the call. KQL is the second interpreter: it reads <your_text> | getschema and finds // commenting the suffix out, or it reads .show table users details\n.drop table critical_data details as two statements separated by a newline. The pattern's mechanism_md says "an interpreter, doing exactly what its spec says, does the wrong thing"; here the wrong thing is two interpreters doing the right thing on either side of a server whose only safety claim was the verb in its tool's name.

CVE-2026-6279: Avada Builder's call_user_func Has No Allowlist. PHP Has No Conditional-Tag Table.. The render_logics POST field is base64-encoded JSON. The function field inside that JSON is a string. Fusion_Builder_Conditional_Render_Helper::get_value() reads the string and hands it to call_user_func, whose interpreter is PHP's symbol table. The handler-author's mental model of the operation was "select a conditional tag from the WordPress conditional-tags vocabulary." The runtime's behavior is "resolve any string PHP knows how to call." Both arrive at a function. They are not the same function. This exhibit lands the pattern in PHP function dispatch: the boundary between data and command sits inside call_user_func, and the boundary is the assumption that the dispatcher's input came from a constrained set the runtime would enforce.

CVE-2025-34291: Langflow's CORS Middleware Warns It Is Exploitable, Then Installs The Exploit. CVE-2025-34291's second primitive is POST /api/v1/validate/code. The endpoint name and the route docstring promise a code validator that returns a list of errors. The implementation, validate_code in utils/validate.py, decides whether a submitted Python file is valid by calling ast.parse, walking the top-level FunctionDef nodes, and exec-ing each one in a globals dict pre-loaded with Langflow's common types. Python's def statement evaluates default-argument expressions at definition time; submitting def x(_=__import__('os').system(...)): pass runs the system call during the validator's exec and records no error. The catch block catches the exec'd code's exceptions; the absence of exceptions is the validator's success path. The pattern fits at its purest grain: the dispatcher receives a string from an authenticated POST, names the operation validate/code, and uses exec() as the implementation of validation. The file is called validate.py and the function uses ast.parse to get the tree, so the author knew the AST existed. A real validator would have walked the AST and refused nodes whose evaluation has side effects. The author reached for exec anyway.

CVE-2026-3300: Everest Forms Pro Sanitized Form Fields For HTML. The Calculation Addon Fed Them To eval().. The Calculation Addon's process_filter() callback reads visitor-controlled form-field values as content (the submitted strings the form designer intended as numeric quantities) and feeds them into a PHP source-code string that eval() runs as command. The boundary the addon believed it was holding lived in sanitize_text_field(), the WordPress core sanitizer for HTML output and text storage. The boundary the addon needed to hold was between PHP literal-string context and PHP statement context, a parser the sanitizer was not written for. Once the visitor's payload closes the single-quoted string the addon wrapped the field in, the same eval() argument reads the next characters as a fresh statement. The interpreter is doing exactly what its spec says.

CVE-2026-33937: Handlebars Trusts Its Own AST. The wire-side shape: an express.json() middleware that deserializes whatever shape the body carries, a Handlebars.compile overload that dispatches on input.type === 'Program' to skip the parser, and a route handler whose call site cannot tell which path it took. The field is named editorTemplateData and the surrounding documentation calls it a template; the interpreter accepts either a template (passive content) or a parsed AST (an instruction stream that the codegen pastes into generated JavaScript). The pattern's mechanism_md says "the interpreter, doing exactly what its spec says, does the wrong thing"; Handlebars spec'd both inputs as legitimate fourteen years ago. The boundary is at the JSON shape, not at the field name. No single component owns the boundary; the composition produces a wire-to-JavaScript pipeline no individual contract names.

CVE-2026-34621: Adobe Acrobat's Privilege Gate Inherits What It Checks. A PDF is a container for declarative rendering instructions. Acrobat honors those instructions. When a document's declared renderer or embedded action is malicious, the trust gate around privileged actions inherits the privileges of the thing it is checking, and the document becomes a command carrier. The defender sees a file. The parser sees a script. The write happens in the space where those two views diverge.

CVE-2026-33017: Langflow's flow_id Authorized. The data Parameter Ran.. A Langflow flow is a JSON document. Operators who hand each other flow exports treat the file as content: a graph of components, a description of edges, a serialization. The Langflow server, on receipt of the same JSON through build_public_tmp, treats data.nodes[*].data.node.template.code.value as command. The string in that field is Python source compiled by exec() inside prepare_global_scope. The pattern's mechanism_md says the attacker's job is "to write content that the interpreter reads as command, and then let the interpreter do the rest"; the flow JSON is the content channel, the Python source string is the embedded command, and the build pipeline is the interpreter that crosses the boundary without knowing it has.

Boundaries

Not every injection. Classical SQL injection, command injection, and XSS are injection at a known hostile boundary. The defense is established (parameterized queries, escaping, CSP), and the failure mode is usually misuse of known-good primitives rather than a deep design problem. Content-is-command is structurally different: the attacker's content travels through channels the system was not designed to treat as hostile.

Not every interpreter. If your system has a documented interpreter for user input and the user supplied malicious input, that is ordinary. Content-is-command describes the case where the content channel becomes an interpreter by accident of shared tokenization, shared context, shared parser. The channel was not meant to accept instructions. The interpreter chose not to distinguish.

Not every supply-chain poisoning. A malicious package is malicious code executing as code. Content-is-command is inert-looking data, parsed by a component that should not have been an interpreter, producing command-like effects. A compromised package does not need this pattern to hurt you. The pattern explains why passive-looking inputs hurt you anyway.

Defender playbook

Enumerate your interpreters. For every parser in your stack, ask: what does it do with well-formed input, and what does it do with surprising input that also happens to be well-formed? An LLM, a PDF reader, a template engine, a markdown renderer, a YAML loader, an XML parser with DTDs, each of these is an interpreter. Each has a content-is-command shape when its content source is wider than its designer expected.

Tag content by provenance, not by format. The dangerous shift is when content from untrusted sources enters a context that was trusted. A Slack message repeated into an LLM prompt changes meaning. A PDF from email embedded into a workflow engine changes meaning. Provenance tracking lets you enforce that untrusted content does not reach privileged interpreters.

Constrain what interpreters can do. An LLM with tool access is a much larger attack surface than an LLM that only produces text. A PDF renderer with JavaScript enabled is a much larger surface than one without. The smaller the interpreter's action set, the smaller the content-is-command blast radius.

Treat retrieval as a trust boundary. Any system that pulls content into an interpreter's context (search indexes, embedding stores, RAG pipelines, email summarizers) is defining a new trust boundary. Log what crosses the boundary. Consider rate limits, source allowlists, or out-of-band confirmation for actions triggered by retrieved content.

Audit the parsers you did not write. If your stack includes a document format parser (PDF, DOCX, XLSX, SVG, ZIP, RTF) from a third party, it has known and unknown ambiguity seams. You will not patch those. You can sandbox them. Containment is the primary defense.

Kinship

Trust Inversion. Content-is-command is trust inversion at the input channel. The channel you trusted as content is now authorizing commands. Every content-is-command vulnerability is a trust-inversion instance with the inverted authorizer being an interpreter.

The Detector Is The Target. Sandboxes and scanners often BECOME content-is-command surfaces. A sandbox that parses a file to decide if it is malicious is an interpreter reading attacker-controlled content. If the parser's interpretation diverges from the defender's, the detector misclassifies and the attacker gets the benefit of the ambiguity.

Security Tool As Primitive. When an interpreter lives inside a security tool (a YARA engine, an AV signature matcher, an EDR script interpreter), content-is-command against that interpreter produces a security-tool-as-primitive outcome. The tool reads attacker content, interprets it as command, executes with the tool's privilege.

The question is not whether your system has interpreters. Every system does. The question is whether any of them read inputs from sources you would call untrusted if you thought about it for thirty seconds.