Summary
Guard Core 3.12.0 blocks a suspicious JSON value when the body is UTF-8, but silently skips body inspection and allows the same JSON value when its bytes are BOM-marked UTF-16 or UTF-32. The downstream FastAPI application accepts and parses those bytes as the original JSON value.
The scanner's UTF-8 decode error is converted into a detection miss before body patterns run, allowing request processing to continue without body-pattern evaluation. The directly demonstrated impact is bypass of an enabled request-body penetration-detection control. The bypass is reproduced independently of any XSS or SQL-injection vulnerability in FastAPI itself.
Details
Expected and actual behavior
Guard Core exposes penetration detection as a security feature. In FastAPI Guard, request-body scanning is active by default and documented for JSON/form bodies and signatures such as XSS and SQL injection. When a JSON body is accepted by the downstream parser, the request-body security control must either inspect the equivalent value or reject the unsupported representation. In Guard Core 3.12.0, a preliminary byte-decoding failure instead becomes a detection miss before body inspection.
With active detection, the exact JSON value <script>audit_marker()</script> produces these results:
| Request body |
Guard Core decision through FastAPI Guard 7.6.0 |
FastAPI endpoint |
| UTF-8 JSON |
HTTP 400, Suspicious activity detected |
Not executed |
| BOM UTF-16 JSON |
HTTP 200 |
Receives the original string |
| BOM UTF-32 JSON |
HTTP 200 |
Receives the original string |
The PoC sets only enable_redis=False and enable_rate_limiting=False so that no external service is needed. It asserts that enable_penetration_detection is still True and passive_mode is still False, then counts handler calls. The handler-call counts confirm that the UTF-8 request is stopped before application code and that the two BOM cases reach it.
Root cause
In Guard Core 3.12.0, detect_penetration_attempt() uses this fail-open path before calling _scan_request_body():
body_bytes = await _read_capped_body(request, config)
if body_bytes is None:
return _build_detection_miss()
try:
raw_body = body_bytes.decode() # Python default: UTF-8
except Exception:
return _build_detection_miss()
bytes.decode() without an encoding uses UTF-8. BOM-marked UTF-16 and UTF-32 JSON take the exception path, and the pattern scan is never called. Starlette's Request.json() supplies raw body bytes to json.loads(body), while Python documents that json.loads() accepts UTF-8, UTF-16, and UTF-32 byte input. The downstream parser therefore accepts bytes discarded by the security layer.
Scope and attribution
The vulnerable decode path is in Guard Core 3.12.0. FastAPI Guard 7.6.0 depends on Guard Core and invokes this security pipeline; its released wheel reproduces the result. The FastAPI Guard adapter's byte-preserving body replay is not the root cause.
Guard Core 3.12.0 is confirmed affected from the released wheel. Earlier Guard Core versions and other adapters were not independently verified.
Validation
- Ran the assertion PoC with released
fastapi-guard 7.6.0 and released guard-core 3.12.0: UTF-8 control 400; UTF-16/UTF-32 cases 200.
- Re-ran default-active configuration: detection
True, passive mode False, and handler-call counts 0, 1, 2 for UTF-8, UTF-16, and UTF-32.
- Reproduced the same outcomes through loopback Uvicorn and raw HTTP/1.1 requests generated with
http.client, rather than relying only on ASGI in-process transport.
- Verified the Guard Core failure path and Starlette's raw-byte JSON path.
- Reproduced the behavior on FastAPI Guard 4.4.1, 5.0.0, and 7.6.0; repeated the current default-configuration replay under Python 3.10, 3.11, and 3.14.
- Checked release 3.12.0, public advisories, and public issue/PR search terms
UTF-16, UTF-32, BOM, charset, and encoding. No public report of this exact decoding differential was found.
The public Guard Core issue GHSA-c2r5-9jw9-m8q5 is distinct: it addressed unbounded chunked-body buffering and is patched in 3.12.0. In 3.12.0, the issue described here is body decode errors being converted into detection misses before body-pattern evaluation.
Standards caveat and remediation
RFC 8259 section 8.1 requires UTF-8 for JSON exchanged between open systems and says networked JSON generators must not add a BOM. The PoC's UTF-16/UTF-32 requests are therefore not standards-conforming network JSON. However, the downstream parser accepts these representations while Guard Core skips body inspection.
A client can send these bytes in one request, and the downstream parser accepts them. At minimum, decoding failures for request bodies subject to penetration detection should fail closed rather than being converted into a detection miss. For JSON media types, a broader fix is to parse the original body bytes using semantics compatible with the downstream JSON parser and recursively inspect string values. Add regression tests for UTF-8 and BOM UTF-16/UTF-32 equivalents of the same signature.
PoC
Save this as poc.py outside either project checkout.
import asyncio
import json
from typing import Annotated
from fastapi import Body, FastAPI
from guard import SecurityConfig
from guard.middleware import SecurityMiddleware
from httpx import ASGITransport, AsyncClient
async def post(app: FastAPI, body: bytes) -> tuple[int, object]:
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://audit.local"
) as client:
response = await client.post(
"/json", content=body, headers={"Content-Type": "application/json"}
)
try:
parsed: object = response.json()
except ValueError:
parsed = response.text
return response.status_code, parsed
async def main() -> None:
app = FastAPI()
handler_calls = 0
@app.post("/json")
async def endpoint(
payload: Annotated[dict[str, str], Body()],
) -> dict[str, object]:
nonlocal handler_calls
handler_calls += 1
return {"received": payload["input"], "handler_calls": handler_calls}
# These settings only remove Redis and unrelated rate limiting.
config = SecurityConfig(enable_redis=False, enable_rate_limiting=False)
assert config.enable_penetration_detection is True
assert config.passive_mode is False
app.add_middleware(SecurityMiddleware, config=config)
value = "<script>audit_marker()</script>"
# Control: the active scanner blocks this literal value.
status, result = await post(app, json.dumps({"input": value}).encode("utf-8"))
assert (status, result, handler_calls) == (
400,
"Suspicious activity detected",
0,
)
# Same JSON object/value; these codecs include a BOM.
status, result = await post(app, json.dumps({"input": value}).encode("utf-16"))
assert (status, result, handler_calls) == (
200,
{"received": value, "handler_calls": 1},
1,
)
status, result = await post(app, json.dumps({"input": value}).encode("utf-32"))
assert (status, result, handler_calls) == (
200,
{"received": value, "handler_calls": 2},
2,
)
print("confirmed: UTF-8 blocked; equivalent UTF-16/UTF-32 bodies accepted")
asyncio.run(main())
Run it with pinned released packages:
uv run --isolated --no-project \
--with 'fastapi-guard==7.6.0' \
--with 'guard-core==3.12.0' \
--with httpx \
python poc.py
Expected result:
confirmed: UTF-8 blocked; equivalent UTF-16/UTF-32 bodies accepted
Impact
An unauthenticated network client can cause active Guard Core request-body penetration detection to allow a request without evaluating body signatures, while the downstream FastAPI endpoint receives the attacker-controlled value. The client needs one Content-Type: application/json request with a BOM-marked UTF-16 or UTF-32 body; no credentials, proxy behavior, Redis, race, large body, or secret is required.
Affected users are applications using Guard Core request-body detection through FastAPI Guard or another adapter that passes raw body bytes to this function. Applications that rely on this layer to block known input attacks lose that protection for this representation. Application-level CIA impact beyond the detection bypass depends on a separately exploitable downstream sink.
Reporter validation request
The fix ships in guard-core 3.13.0, now on PyPI. If you reported this issue, please install 3.13.0 and confirm it is resolved. You can reply in this advisory's discussion thread or open a new issue with the result.
Summary
Guard Core 3.12.0 blocks a suspicious JSON value when the body is UTF-8, but silently skips body inspection and allows the same JSON value when its bytes are BOM-marked UTF-16 or UTF-32. The downstream FastAPI application accepts and parses those bytes as the original JSON value.
The scanner's UTF-8 decode error is converted into a detection miss before body patterns run, allowing request processing to continue without body-pattern evaluation. The directly demonstrated impact is bypass of an enabled request-body penetration-detection control. The bypass is reproduced independently of any XSS or SQL-injection vulnerability in FastAPI itself.
Details
Expected and actual behavior
Guard Core exposes penetration detection as a security feature. In FastAPI Guard, request-body scanning is active by default and documented for JSON/form bodies and signatures such as XSS and SQL injection. When a JSON body is accepted by the downstream parser, the request-body security control must either inspect the equivalent value or reject the unsupported representation. In Guard Core 3.12.0, a preliminary byte-decoding failure instead becomes a detection miss before body inspection.
With active detection, the exact JSON value
<script>audit_marker()</script>produces these results:Suspicious activity detectedThe PoC sets only
enable_redis=Falseandenable_rate_limiting=Falseso that no external service is needed. It asserts thatenable_penetration_detectionis stillTrueandpassive_modeis stillFalse, then counts handler calls. The handler-call counts confirm that the UTF-8 request is stopped before application code and that the two BOM cases reach it.Root cause
In Guard Core 3.12.0,
detect_penetration_attempt()uses this fail-open path before calling_scan_request_body():bytes.decode()without an encoding uses UTF-8. BOM-marked UTF-16 and UTF-32 JSON take the exception path, and the pattern scan is never called. Starlette'sRequest.json()supplies raw body bytes tojson.loads(body), while Python documents thatjson.loads()accepts UTF-8, UTF-16, and UTF-32 byte input. The downstream parser therefore accepts bytes discarded by the security layer.Scope and attribution
The vulnerable decode path is in Guard Core 3.12.0. FastAPI Guard 7.6.0 depends on Guard Core and invokes this security pipeline; its released wheel reproduces the result. The FastAPI Guard adapter's byte-preserving body replay is not the root cause.
Guard Core 3.12.0 is confirmed affected from the released wheel. Earlier Guard Core versions and other adapters were not independently verified.
Validation
fastapi-guard 7.6.0and releasedguard-core 3.12.0: UTF-8 control 400; UTF-16/UTF-32 cases 200.True, passive modeFalse, and handler-call counts0,1,2for UTF-8, UTF-16, and UTF-32.http.client, rather than relying only on ASGI in-process transport.UTF-16,UTF-32,BOM,charset, andencoding. No public report of this exact decoding differential was found.The public Guard Core issue
GHSA-c2r5-9jw9-m8q5is distinct: it addressed unbounded chunked-body buffering and is patched in 3.12.0. In 3.12.0, the issue described here is body decode errors being converted into detection misses before body-pattern evaluation.Standards caveat and remediation
RFC 8259 section 8.1 requires UTF-8 for JSON exchanged between open systems and says networked JSON generators must not add a BOM. The PoC's UTF-16/UTF-32 requests are therefore not standards-conforming network JSON. However, the downstream parser accepts these representations while Guard Core skips body inspection.
A client can send these bytes in one request, and the downstream parser accepts them. At minimum, decoding failures for request bodies subject to penetration detection should fail closed rather than being converted into a detection miss. For JSON media types, a broader fix is to parse the original body bytes using semantics compatible with the downstream JSON parser and recursively inspect string values. Add regression tests for UTF-8 and BOM UTF-16/UTF-32 equivalents of the same signature.
PoC
Save this as
poc.pyoutside either project checkout.Run it with pinned released packages:
Expected result:
Impact
An unauthenticated network client can cause active Guard Core request-body penetration detection to allow a request without evaluating body signatures, while the downstream FastAPI endpoint receives the attacker-controlled value. The client needs one
Content-Type: application/jsonrequest with a BOM-marked UTF-16 or UTF-32 body; no credentials, proxy behavior, Redis, race, large body, or secret is required.Affected users are applications using Guard Core request-body detection through FastAPI Guard or another adapter that passes raw body bytes to this function. Applications that rely on this layer to block known input attacks lose that protection for this representation. Application-level CIA impact beyond the detection bypass depends on a separately exploitable downstream sink.
Reporter validation request
The fix ships in guard-core 3.13.0, now on PyPI. If you reported this issue, please install 3.13.0 and confirm it is resolved. You can reply in this advisory's discussion thread or open a new issue with the result.