|
DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
|
A separately-runnable harness that attacks the server the way a hostile client would, and asserts the guarantees that keep a deterministic device safe. It is not part of the per-commit unit-test run (test/run_tests.sh): it is heavier, randomized, and meant to be run on demand or on a schedule.
## ⚖️ Authorized use only
The live driver (pentesting/detws_pentest.py) and the on-device techniques below send real attack traffic. Use them only against a device you own or have explicit, written permission to test. Unauthorized access to or interference with computer systems is a crime in most jurisdictions (US CFAA, UK Computer Misuse Act, and equivalents). You, the operator, are solely responsible for staying within the law and the scope of your authorization. This tooling is provided as is, with no warranty and no liability. We build defensive tooling - use it for good. The live driver refuses to run until you pass
--authorized.
There are three layers:
env:native_pentest) - throws malformed, oversized, partial, binary, and deterministically-random input at the untrusted-input parsers and checks the safety invariants. Fast, reproducible, CI-friendly.curl/nc techniques for one-off probing.Every adversarial input must preserve all three. A violation is a finding.
| Invariant | What it means | How it is checked |
|---|---|---|
| Fixed footprint | No fixed buffer index ever exceeds its compile-time bound (no overflow). | After every input: path_idx <= MAX_PATH_LEN, header_count <= MAX_HEADERS, body_len <= BODY_BUF_SIZE, response length <= cap, etc. |
| Fail-closed | Bad input lands in a defined error state, never an undefined one. | Parser state is always within the ParseState enum; bad requests reach PARSE_ERROR (400) / PARSE_ENTITY_TOO_LARGE (413) / PARSE_URI_TOO_LONG (414). |
| Liveness | The parser always terminates - no hang, no over-read. | The harness feeds bounded streams and returns; a hang/crash/sanitizer trip fails the run. |
These follow from the library's core promise: no heap after begin(), all buffers statically sized, so the only way adversarial input can hurt the device is an overflow or an undefined state - exactly what this suite hunts.
It is intentionally excluded from test/run_tests.sh, so a normal pio test / CI test run does not pay its cost. Run it before a release, nightly, or after touching any parser.
Targets (every codec that faces untrusted bytes from the wire or the user):
http_parser_feed) - the primary attack surface.multipart_parse) - in-place body parsing.coap_server_process) - untrusted UDP, option parsing.snmp_agent_process, ber_read_*) - untrusted UDP.modbus_process_adu) + master response parser (modbus_parse_response).cbor_read_*) and JSON reader (json_get_*).base64_decode) and base32 (detws_base32_decode) decoders.sqlite_parse_*, sqlite_table_cursor_*, sqlite_read_payload) - a malicious .db file is wholly untrusted: random pages, a garbage b-tree the multi-page cursor must walk without hanging (bounded descent over cyclic interior pointers), a hostile overflow-page chain, and structure-aware mutation of a valid image built by the writer.resp_parse) - a hostile server / MITM reply, with the classic lying $/* length-prefix that must never turn into an over-read.opcua_parse_header / _hello / _open / _msg / _read / _write / _browse) - the length-prefixed strings / byte-strings and array counts of an untrusted UA-TCP peer: random bodies behind a valid UACP header, an OPN whose SecurityPolicyUri length lies (huge / negative / over-long), and size-field mismatches per message type. The captured NodesToRead/Write/Browse counts must stay clamped to their DETWS_OPCUA_*_MAX arrays. (This target's first run found three latent signed-overflow UBs in the shared number parsers - see docs/BUGS.md.)dns_server_build_response) - an untrusted query: the QNAME label parse must not over-read (label length past the buffer, missing root label, compression pointers rejected in a question) and the response builder must never write past a random out_cap (an exact-size buffer makes ASan enforce it).dnp3_parse_frame) - an untrusted SCADA frame: a lying LEN / block layout behind the 0x0564 start word must not over-read or overflow the user-data buffer.stomp_parse_frame) - every returned slice (command, header keys/values, body) must point inside the source buffer, and header_count stay clamped.smb2_parse_negotiate_response / _session_setup_response / _tree_connect_response / _create_response / _close_response / _read_response / _write_response, smb2_parse_header) - a malicious SMB server: random bodies behind a valid sync header exercise the StructureSize checks and the security-buffer / read-data offset+length bounds, and every returned pointer (the SPNEGO/NTLM security buffer, the read data) must stay inside the message.spnego_parse_response, ntlmssp_parse_challenge) - a hostile authentication blob: the definite-length DER walk and the AV_PAIR / target-info fields must never over-read on a lying tag/length, and the returned responseToken / target-info slice must stay in bounds.dnc_decode_feed) - a hostile drip-feed stream (ISO + EIA tape codes): the block reassembler must never overrun its line buffer (len <= DETWS_DNC_LINE_MAX) on any byte sequence.ftp_parse_reply / ftp_parse_pasv / ftp_parse_epsv) - a malicious / MITM FTP server: a lying (h1,h2,h3,h4,p1,p2) PASV tuple or a bad multiline reply must not over-read, and consumed never exceeds the buffer.fins_parse_response, melsec_parse_response, cip_parse_response, eip_parse_send_rr_data, df1_parse_frame) - a malicious PLC / controller reply: a lying length / item count must not over-read, every returned payload slice stays inside the frame, and DF1's DLE de-stuffing never writes past the caller's out_cap.bvlc_parse, npdu_parse, tpkt_parse, cotp_parse) - untrusted building-automation / SCADA framing: the BVLC length, the NPDU control-flag-driven layout, and the TPKT/COTP length fields must not over-read, and every returned NPDU / APDU / user-data slice stays inside the frame.c37118_parse_frame, c37118_parse_command, fins_parse_command) - an untrusted grid synchrophasor frame / PLC request: a lying FRAMESIZE or command-parameter length must not over-read; the returned data / parameter slice stays inside the frame.jwt_verify_hs256, jwt_time_valid) - a forged / malformed bearer token: the header.payload.sig split, the base64url decode, and the exp/nbf claim scan must stay bounded and reject cleanly (no over-read on a lying segment).detws_dnet_data_parse, detws_cclink_parse, amqp_parse_frame, detws_mms_parse) - untrusted DirectNET / CC-Link / AMQP / MMS input: a lying length must not over-read, consumed never exceeds the buffer, and every returned payload / service-body slice stays inside the frame.detws_rtps_parse) - an untrusted UDP multicast datagram: behind the RTPS magic, a lying submessageLength must not walk the submessage cursor out of bounds, and every submessage body surfaced to the callback stays inside the datagram.Attack classes (see test/test_pentest/test_pentest.cpp):
Content-Length → must reach 414/413, never overflow.header_count clamps to MAX_HEADERS.Content-Length, high bytes in the path, the HTTP/2 PRI * HTTP/2.0 preface.Content-Length, CL.TE / TE.CL, obfuscated Transfer-Encoding, negative/hex/overflowing lengths, bad chunk sizes.0x1234567), bounded-checked after each.SEQUENCE) with random bytes spliced into the deeper fields, so the option/length/tag walkers are driven past the outer frame.The fuzz PRNG is a deterministic xorshift32 seeded in setUp(), so a failure is fully reproducible: re-run native_pentest and it replays the identical streams. To explore a different corpus, change the seed in test_pentest.cpp.
The host build can run under AddressSanitizer / UndefinedBehaviorSanitizer to turn a silent over-read or UB into a hard failure. Add to the env's build_flags / link flags:
(Available on Linux/macOS clang or gcc; the same toolchain note as native_tsan applies - not available on the MSVC-target Windows host, so run it in CI/WSL.)
The pattern is three lines per parser:
The HTTP, multipart, CoAP, SNMP/BER, Modbus (slave + master), CBOR, MessagePack, JSON, base64, base32, the SQLite on-disk reader, the Redis RESP decoder, the OPC UA Binary parsers, the base-10 number parsers (det_strtol/_strtoul/ _strtof), the GraphQL query parser, the DNS server query parse, the DNP3 data-link frame, and the STOMP frame parser are covered. Good candidates still open: the WebSocket frame reassembler (ws_parse) once a host harness for its rx ring exists (its per-byte core ws_feed_byte dispatches into the transport/session layer, so it needs those mocks wired in), and the WebDAV binary decoder.
The host fuzz proves the parsers are safe in isolation; the live driver proves the running device is - its connection pool caps, its throttles and lockout engage, it fails closed on the wire, and its free heap does not drift (the "no heap after `begin()`" promise). It is feature-aware: point it at a device and it reads /diag to learn which features are built in, then runs only the applicable attacks.
It checks two oracles around every attack - liveness (a legitimate GET / still succeeds) and heap-drift (sampled from /health, /stats, /metrics, or /diag) - plus per-attack fail-closed assertions (414/413/401/429/403/416/...). The exit code is non-zero when any finding exists, so it drops into a CI gate. It is Python-3-stdlib-only (no pip installs). Full documentation, the attack catalog, and how to extend it: pentesting/README.md.
Flash a build with the listeners and defenses you want to exercise, then drive it from a host on the same network. The goal: confirm the device stays up and fails closed under abuse, within its fixed footprint.
Enable the relevant defenses in ServerConfig.h / build flags:
DETWS_ENABLE_ACCEPT_THROTTLE (+ DETWS_ACCEPT_THROTTLE_MAX/_WINDOW_MS)DETWS_ENABLE_PER_IP_THROTTLEDETWS_ENABLE_AUTH_LOCKOUTDETWS_ENABLE_IP_ALLOWLISTDETWS_ENABLE_CSRFDETWS_ENABLE_GUARDRAILS → /health, or DETWS_ENABLE_METRICS) before/during/after - free_heap must not drift.Modbus, SNMP, and CoAP have no authentication - only expose them on a trusted network, and pentest accordingly (malformed PDUs, oversized datagrams).
The suite is a floor, not a ceiling - widen the corpus, add targets, and run it under sanitizers in CI for the strongest assurance.