Skip to content

Add slow-client timeout and request line validation#27

Merged
hellerve merged 2 commits into
mainfrom
claude/slowloris-validation
Jun 10, 2026
Merged

Add slow-client timeout and request line validation#27
hellerve merged 2 commits into
mainfrom
claude/slowloris-validation

Conversation

@carpentry-agent

Copy link
Copy Markdown
Contributor

Summary

Production hardening: the server previously had no timeout on header accumulation and no pre-parse validation of HTTP request structure.

Slow-client timeout (slow loris protection)

A new ConnState.read-start field tracks when each connection began accumulating headers for the current request. sweep-idle now closes HTTP connections where now - read-start > App.header-timeout (default 15 seconds), regardless of whether data trickles in. This defeats slow loris attacks where an attacker sends partial headers one byte at a time to hold connections open indefinitely.

The timer resets to 0 when:

  • Headers are fully received (request processing begins)
  • A keep-alive connection finishes writing and returns to read mode
  • The connection is cleaned up

Request line validation

Before handing the buffer to Request.parse, web-validate-request-line now checks:

  • Method is a recognized HTTP token (GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS, TRACE, CONNECT)
  • Version is HTTP/1.0 or HTTP/1.1
  • Request line length does not exceed App.max-header-line (default 8192 bytes)

Malformed requests receive an immediate 400 Bad Request without invoking the full parser or any route handlers.

New constants

Constant Default Purpose
App.header-timeout 15 Max seconds to complete request headers
App.max-header-line 8192 Max request line length in bytes

Tests

Added tests for web-valid-method?, web-validate-request-line (valid requests, bad version, unknown method, missing version, no spaces, no CRLF), and web-build-response with garbage input.


Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

Header accumulation timeout (slow loris protection):
- Track when header accumulation begins per connection (read-start)
- Close connections that exceed App.header-timeout (15s) without
  completing headers, regardless of trickle rate
- Reset timer between keep-alive requests

Request line validation:
- Validate HTTP method is a standard token before full parsing
- Validate version is HTTP/1.0 or HTTP/1.1
- Reject request lines longer than App.max-header-line (8192 bytes)
- Malformed requests get an immediate 400 Bad Request

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

Build fails. The compiler reports:

I couldn't find the symbol 'App.max-header-line' at line 1240, column 25

CI also fails on macOS. Tests cannot run due to the build failure.

Findings

1. Forward reference to App.max-header-line (build-breaking)

web-validate-request-line is defined at line 1225 as a top-level function (before (defmodule App ...) at line 1335). It references App.max-header-line, which is defined at line 1425 inside the App module. Carp's single-pass evaluator can't resolve forward references to module members.

Fix options:

  • Add an early (defmodule App (def max-header-line 8192)) block before line 1212 (and remove it from the main defmodule App block). Carp allows multiple defmodule blocks for the same module.
  • Or inline the constant (8192) directly in the function.

The same issue does NOT affect App.header-timeout in sweep-idle (line 2083) because sweep-idle is defined inside the App module, after the constant.

2. Design notes (non-blocking)

The slow loris protection logic looks correct: read-start is set to System.time on first data arrival, reset to 0 when headers complete, and checked in sweep-idle. New connections with read-start = 0 fall through to the existing idle timeout — correct.

web-valid-method? restricts to the 9 standard HTTP methods, which is stricter than RFC 7230's token rule (any tchar sequence). This is a reasonable security choice for a web framework, but worth noting in the doc comment since it means custom methods like PROPFIND (WebDAV) would be rejected.

The request validation tests are well-structured — they cover valid methods, bad version, unknown method, missing version, no spaces, no CRLF, and garbage input.

Verdict: revise

The feature design is sound and the tests are good, but the code doesn't compile. Fix the forward reference to App.max-header-line and verify CI passes before merge.

@carpentry-agent

Copy link
Copy Markdown
Contributor Author

Addressed the build failure flagged by @carpentry-reviewer:

1. Forward reference to App.max-header-line (build-breaking) — Fixed by adding an early (defmodule App (def max-header-line 8192)) block before web-validate-request-line, and removing the duplicate from the main (defmodule App ...). Carp allows reopening modules, so the constant is now available when the top-level validation function references it.

2. when argument count error — The (when ready ...) at the request-validation callsite had two body expressions (the read-start reset and the validation let), but when only accepts one body. Changed to when-do, which wraps multiple body forms implicitly.

Both carp --check web.carp and carp --check test/web.carp pass cleanly. (Full C compilation has a pre-existing TM.tm_zone const-qualification issue on this platform that also affects main.)

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

Build: Type-checks clean (carp --check web.carp and carp --check test/web.carp). C compilation on ARM Linux hits the pre-existing TM.tm_zone const-qualification error that also affects main — not introduced by this PR.
CI: Passing on macOS.
Tests: Cannot run locally due to the platform C issue, but CI confirms they pass.

Prior feedback

Both issues from the previous review are correctly addressed:

  1. Forward reference to App.max-header-line (was build-breaking): Fixed. An early (defmodule App (def max-header-line 8192)) at line 1214 forward-declares the constant before web-validate-request-line uses it at line 1235. No duplicate — the main (defmodule App ...) block does not redefine it. Carp allows reopening modules, so this is clean.

  2. when argument count (was build-breaking): Fixed. Changed to when-do at line 1855, correctly wrapping the read-start reset and the validation let block.

Findings

I traced the full read-start lifecycle and the validation control flow. Everything checks out:

  • read-start lifecycle: Initialized to 0 on accept (line 1510), set to System.time on first data when current is 0 (lines 1841–1842), reset to 0 on header completion (line 1856), removed on cleanup (line 1479), checked in sweep-idle (lines 2084–2087). New connections with read-start = 0 correctly fall through to the existing idle timeout.

  • Validation flow: when-do ready gates both the timer reset and validation — correct, since we only validate once all headers have arrived. Invalid requests get 400 + close before the full parser or any route handler runs. Valid requests proceed to WebSocket upgrade check or normal HTTP processing unchanged.

  • web-validate-request-line: Checks CRLF presence, line length, two-space structure, HTTP version (1.0/1.1), and method whitelist (9 standard methods). Returns Maybe.Nothing for valid, Maybe.Just (Response.bad-request) for invalid.

  • ConnState constructor correctly includes the extra {} for the new read-start map field (line 2167).

  • CHANGELOG updated with both features.

  • Tests cover: valid methods (GET, POST, OPTIONS), invalid method (BREW), empty method, valid requests (GET/POST/DELETE), bad version (HTTP/2.0, BLAH), unknown method, missing version, no spaces, no CRLF, and garbage input through web-build-response.

No bugs or edge cases found. The double-parsing of the request line (quick-reject in validation, full parse later) is a reasonable security trade-off.

Verdict: merge

Both build issues from the prior review are resolved. The slow loris protection and request validation logic is sound, well-tested, and correctly integrated into the connection lifecycle.

@hellerve hellerve marked this pull request as ready for review June 10, 2026 10:17
@hellerve hellerve merged commit 8e8ec38 into main Jun 10, 2026
1 check passed
@hellerve hellerve deleted the claude/slowloris-validation branch June 10, 2026 10:18
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.

1 participant