Skip to content

http: server-side Digest Auth with htdigest file support - #3638

Open
bgermann wants to merge 1 commit into
warmcat:mainfrom
bgermann:htdigest
Open

http: server-side Digest Auth with htdigest file support#3638
bgermann wants to merge 1 commit into
warmcat:mainfrom
bgermann:htdigest

Conversation

@bgermann

Copy link
Copy Markdown

Add server-side HTTP Digest Auth support using the standard htdigest file format (username:realm:HA1_hex per line).

  • New mount auth mode LWSAUTHM_DIGEST_AUTH
  • New mount field basic_auth_realm for the auth realm
  • lws_check_digest_auth() parses Authorization: Digest headers and validates against the htdigest file
  • lws_unauthorised_digest_auth() sends 401 with a fresh random nonce
  • Nonce is stored per-wsi and verified on keep-alive retries
  • Constant-time response comparison via lws_timingsafe_bcmp
  • Wired into HTTP/1.1, HTTP/2 and WS upgrade paths
  • Added minimal example minimal-http-server-digestauth
  • LWS_WITH_HTTP_DIGEST_AUTH now implies LWS_WITH_HTTP_BASIC_AUTH so the shared lws_authorization_rewrite helper is available

@lws-team

Copy link
Copy Markdown
Member

Gemini says:

PR 3638 Security & Robustness Analysis
This PR introduces an implementation of HTTP Digest Authentication. However, there are several critical security vulnerabilities and robustness issues that need to be addressed before it can be merged.

Critical Security Vulnerabilities

  1. Unbounded Replay Attack (Nonce Bypass)
    The server completely bypasses nonce validation if it hasn't stored a nonce for the connection, allowing indefinite replay attacks.

c

if (wsi->http.digest_auth_nonce &&
    strcmp(nonce_in, wsi->http.digest_auth_nonce)) {
	lwsl_err("digest auth: nonce mismatch\n");
	return LCBA_FAILED_AUTH;
}

Because wsi->http.digest_auth_nonce is NULL for any new connection (and is also set to NULL after the first request on a keep-alive connection), the strcmp is entirely skipped. The server then uses the client-provided nonce_in to verify the MD5 hash. An attacker can intercept any valid Authorization: Digest ... header and replay it on a newly established connection to bypass authentication entirely.

  1. Cross-URI Replay Attack (Missing URI Validation)
    The server computes HA2 using the uri parameter provided in the client's Authorization header (uri_in), but never validates that this matches the actual request URI (uri_ptr).

c

/* HA2 = MD5(method ":" uri) -- use uri from auth header per RFC 2617 */
n = lws_snprintf(tmp, sizeof(tmp), "%s:%s",
		 method, uri_in[0] ? uri_in : "/");

An attacker who intercepts a valid authentication header for a low-privilege endpoint (e.g., /public) can send a request to a high-privilege endpoint (e.g., /secret) while keeping uri="/public" in the header. The server will compute a matching hash and grant access to /secret as the authenticated user.

Robustness & Protocol Issues
3. Breaks HTTP Keep-Alive / Pipelining
The code discards the nonce immediately after a single successful authentication:

c

/* Nonce has been consumed; discard stored value */
lws_free_set_NULL(wsi->http.digest_auth_nonce);

In Digest Auth, a client is supposed to reuse the nonce for subsequent requests on the same connection by incrementing the nonce count (nc). By deleting the nonce, if the NULL-bypass vulnerability (Issue 1) is fixed, the server will force a new 401 challenge on every single request over a keep-alive connection, destroying performance.

  1. No Nonce Count (nc) Tracking
    The server does not track the nc (nonce count) field. Even if nonces were persisted per-connection correctly, an attacker could replay the exact same request over the same connection without the server detecting the reuse of the nc value.

  2. Synchronous File I/O Blocks Event Loop
    lws_find_htdigest_ha1 uses blocking open and read on the htdigest_file. While this mimics the existing basic_auth implementation, it is an architectural robustness issue. If the password file is on a slow filesystem (like an NFS mount), reading it synchronously will stall the entire LWS event loop and pause all other connections.

  3. Missing EINTR Handling on read()
    The read loop inside lws_find_htdigest_ha1 aborts if n <= 0:

c

	if (pos == n) {
		n = (int)read(fd, buf, sizeof(buf));
		if (n <= 0) { ... break; }

If the read() system call is interrupted by a signal (errno == EINTR), it will return -1. The code treats this as a failure and spuriously denies authentication instead of retrying the read.

Recommendations
To properly implement Digest Auth in a stateless, event-driven server like LWS:

Use Signed Nonces: Instead of random strings, generate nonces as base64(timestamp + ":" + HMAC(secret, timestamp)). This allows the server to verify the nonce was issued by itself and check expiration, without needing to store nonces per-wsi.
Validate the URI: Explicitly check that uri_in matches the requested URI (uri_ptr), or reject the request if they differ.
Handle EINTR: Wrap the read() call in a loop that checks for EINTR.

@bgermann

Copy link
Copy Markdown
Author

I have addressed the Gemini comments except 5 which is an architectural change.

@lws-team

Copy link
Copy Markdown
Member

It's definitely improved thanks. Gemini finds two problems

  1. Broken Query Parameters (Regression): To fix the cross-URI replay attack, the author added a strict strcmp(uri_in, uri_ptr). But in libwebsockets, uri_ptr holds only the path (query parameters are stripped during parsing and stored in WSI_TOKEN_HTTP_URI_ARGS). Because the RFC mandates the client's uri field must exactly match the Request-Line, a valid request like GET /secret?foo=bar will be incorrectly rejected by the server because "/secret?foo=bar" does not strictly match "/secret". The server must reconstruct the full URI before comparing it.

  2. Same-Connection Replay via Downgrade Attack: The server still accepts legacy Digest Auth responses (where the qop parameter is omitted). If an active MITM attacker strips qop="auth" from the server's 401 challenge, the client will omit the nc (Nonce Count) field in its response. Because the server bypasses the nc replay check if the field is missing, the attacker can indefinitely replay that legacy response over the same connection. The server should mandate qop="auth" in the client's response.

A good trick to know about LLMs is that if you simply open a new context and ask it to assess the output from the previous context, it won't feel any need to be consistent with the previous context's work, get defensive or freak out. It will just do what Gemini is doing for me when I ask it to assess it, directly to you. If there are gaping holes it will just shamelessly tell you [that patch it just made] has the following gaping holes...

@lws-team
lws-team force-pushed the main branch 6 times, most recently from 558a432 to 4e59faf Compare July 27, 2026 20:02
@bgermann bgermann closed this Jul 27, 2026
@bgermann bgermann reopened this Jul 27, 2026
Add server-side HTTP Digest Auth support using the standard htdigest
file format (username:realm:HA1_hex per line).

- New mount auth mode LWSAUTHM_DIGEST_AUTH
- New mount field basic_auth_realm for the auth realm
- lws_check_digest_auth() parses Authorization: Digest headers and
  validates against the htdigest file
- lws_unauthorised_digest_auth() sends 401 with a fresh random nonce
- Nonce is stored per-wsi and verified on keep-alive retries
- Constant-time response comparison via lws_timingsafe_bcmp
- Wired into HTTP/1.1, HTTP/2 and WS upgrade paths
- Added minimal example minimal-http-server-digestauth
- LWS_WITH_HTTP_DIGEST_AUTH now implies LWS_WITH_HTTP_BASIC_AUTH so
  the shared lws_authorization_rewrite helper is available

Signed-off-by: Bastian Germann <bage@debian.org>
Co-developed-by: Claude Sonnet 4.6
@sonarqubecloud

Copy link
Copy Markdown

@lws-team
lws-team force-pushed the main branch 2 times, most recently from feb2be6 to b1c687c Compare August 5, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants