Skip to content

Commit ed37461

Browse files
committed
Merge TASK-055: sanitize default 500 body / DR-009 Revision 1
2 parents 7e3c77f + 9e6c2e5 commit ed37461

14 files changed

Lines changed: 392 additions & 40 deletions

File tree

README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,19 @@ build, `webserver(create_webserver{}.use_ssl(true))` throws
639639
dispatch. The handler signature is
640640
`http_response(const http_request&, std::string_view message)`. Load-bearing:
641641
see [Error propagation](#error-propagation).
642+
* **`.expose_exception_messages(bool = true)`** — restores the v1
643+
behaviour of including the originating exception message in the
644+
**default** 500 response body. Default is `false`: the default body
645+
is the fixed string `"Internal Server Error"`
646+
([DR-009 Revision 1](specs/architecture/11-decisions/DR-009.md),
647+
CWE-209 information-disclosure fix). The configured `log_error`
648+
callback continues to receive the verbatim message regardless of
649+
this flag — only the HTTP response body is affected.
650+
**Warning:** exception messages routinely contain file paths, SQL
651+
fragments, internal identifiers, and attacker-influenced input.
652+
Enable only in development or behind an explicit `#ifndef NDEBUG`
653+
guard. A configured `internal_error_handler` is unaffected — it
654+
always receives the message and can build any body it wants.
642655
643656
A worked example:
644657
@@ -1664,9 +1677,20 @@ Distilled from
16641677
invokes `internal_error_handler(request, e.what())`. The response
16651678
returned by `internal_error_handler` is sent to the client; if no
16661679
custom `internal_error_handler` is configured, a default 500 with
1667-
the message in the body is sent.
1680+
the **fixed body `"Internal Server Error"`** is sent
1681+
(DR-009 Revision 1, CWE-209 fix — exception text often contains
1682+
file paths, SQL fragments, or attacker-influenced input and must
1683+
not cross a process boundary to an untrusted client). To restore
1684+
the v1 behaviour of including the exception message in the body
1685+
for development, set `.expose_exception_messages(true)` on the
1686+
builder. The verbatim message is still surfaced via the
1687+
`log_error` callback and to any configured
1688+
`internal_error_handler` regardless of the flag.
16681689
2. **A handler that throws something other than `std::exception`** is
16691690
also caught, with `"unknown exception"` substituted for the message.
1691+
The same default body (`"Internal Server Error"`) applies; the
1692+
`"unknown exception"` sentinel reaches the wire only when
1693+
`.expose_exception_messages(true)` is set.
16701694
3. **Library-internal failures during dispatch** (allocation, body
16711695
materialisation) flow through the same `internal_error_handler` path.
16721696
4. **If `internal_error_handler` itself throws**, the library logs and
@@ -1698,7 +1722,13 @@ httpserver::webserver ws{cfg};
16981722
```
16991723
17001724
See [`examples/custom_error.cpp`](examples/custom_error.cpp) for a
1701-
worked example.
1725+
worked example. Note that the snippet above — which echoes `what`
1726+
verbatim to the wire — is illustrative of the explicit-handler case;
1727+
the **default** (no `internal_error_handler` configured) path now
1728+
sends a fixed body, and only the `log_error` callback receives the
1729+
verbatim message. To restore the v1 verbose-body default for
1730+
development, chain `.expose_exception_messages(true)` on the builder
1731+
(see [Custom error handlers](#custom-error-handlers)).
17021732
17031733
[Back to TOC](#table-of-contents)
17041734

RELEASE_NOTES.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,19 @@ and see the v2 replacement.
196196
fluent assembly.
197197
- **`webserver(create_webserver const&)` is `explicit`.** Direct-init
198198
`webserver ws{cw};` rather than copy-init `webserver ws = cw;`.
199+
- **Default internal-error response body is now a fixed string.** v1's
200+
default 500 body included the originating exception's `e.what()`
201+
text. v2.0
202+
([DR-009 Revision 1](specs/architecture/11-decisions/DR-009.md))
203+
sends `"Internal Server Error"` instead, eliminating a CWE-209
204+
information-disclosure surface (exception messages routinely embed
205+
file paths, SQL fragments, internal identifiers, and
206+
attacker-influenced input). The `log_error` callback continues to
207+
receive the verbatim message. To restore the v1 verbose-body
208+
behaviour for development, set `.expose_exception_messages(true)`
209+
on the builder. Configured `internal_error_handler` callbacks are
210+
unaffected — they still receive the message and can build any body
211+
they want.
199212

200213
## Threading
201214

@@ -226,7 +239,11 @@ for the full statement. The load-bearing points for porters:
226239
- A handler that throws lands at the configured
227240
`internal_error_handler` (a `std::function<http_response(const http_request&, std::string_view)>`).
228241
The `string_view` carries `what()` from the caught exception. Default
229-
behaviour: log and return `500`.
242+
behaviour: log and return `500`. When no `internal_error_handler` is
243+
configured, the default response body is the fixed string
244+
`"Internal Server Error"` (DR-009 Revision 1, CWE-209 fix); set
245+
`.expose_exception_messages(true)` on the builder to restore the v1
246+
message-in-body behaviour for development.
230247
- Calling an API entry point whose backend feature is disabled at
231248
configure time (e.g. `ssl_cert(...)` on a `HAVE_GNUTLS=no` build)
232249
throws `feature_unavailable` — a public `std::runtime_error` subclass.

specs/architecture/05-cross-cutting.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
### 5.2 Error propagation
1818

19-
**Contract (committed in DR-9):**
20-
1. Handler throws `std::exception` → caught, logged via the `log_error` callback, `internal_error_handler` invoked with `e.what()`, response sent (default 500).
21-
2. Handler throws non-`std::exception` → caught with `catch (...)`, logged generically via `log_error`, `internal_error_handler` invoked with `"unknown exception"`.
19+
**Contract (committed in DR-9, revised 2026-05-29 — Revision 1):**
20+
1. Handler throws `std::exception` → caught, logged via the `log_error` callback, `internal_error_handler` invoked with `e.what()`, response sent. **Default 500** body is the fixed string `"Internal Server Error"` (DR-009 Revision 1 / CWE-209). The verbatim message is still surfaced via the `log_error` callback and to any configured `internal_error_handler`; the v1 behaviour of including it in the default body is opt-in via `create_webserver::expose_exception_messages(true)` (development only).
21+
2. Handler throws non-`std::exception` → caught with `catch (...)`, logged generically via `log_error`, `internal_error_handler` invoked with `"unknown exception"`. Default body is the same fixed string `"Internal Server Error"`; the `"unknown exception"` sentinel is on the wire only when `expose_exception_messages(true)` is set.
2222
3. Library-internal exception in dispatch (allocation failure, body materialization error) → same path as (1)/(2).
2323
4. `internal_error_handler` itself throws → library logs via `log_error` and sends a hardcoded 500 with empty body.
2424
5. `feature_unavailable` is a normal `std::runtime_error`; no special status mapping. Users who care translate it explicitly.

specs/architecture/11-decisions/DR-009.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
### DR-009: Handler error-propagation contract
22

3-
**Status:** Accepted
3+
**Status:** Accepted; revised 2026-05-29 (Revision 1)
44
**Date:** 2026-04-30
55
**Context:** With DR-4 (return-by-value), null-return is impossible. Two cases remain: handler throws, library-internal exception during dispatch.
66

@@ -19,3 +19,72 @@
1919
- `internal_error_handler` is the single user-overridable error escape hatch.
2020

2121
---
22+
23+
### Revision 1 (2026-05-29) — Sanitize the default 500 body
24+
25+
**Context:** The original decision had `internal_error_page` surface
26+
`e.what()` in the default-handler 500 body for debugging ergonomics.
27+
This was flagged by two security reviewers on TASK-031 (task-031 #3,
28+
task-031 #4) and one Pass-1 sweep on TASK-036 (task-036 #44) as a
29+
CWE-209 information-disclosure surface: handler exception text
30+
routinely embeds file paths, SQL fragments, internal identifiers, and
31+
attacker-influenced input that must not cross a process boundary to an
32+
untrusted client.
33+
34+
**Revised decision:** The default body of the no-handler-configured 500
35+
path is the fixed string `"Internal Server Error"`. The originating
36+
exception message is still:
37+
38+
1. Forwarded verbatim to a user-configured `internal_error_handler` (if
39+
one is wired) — that handler can render any body it wants.
40+
2. Forwarded verbatim to the user-configured `log_error` callback (the
41+
server-side log).
42+
43+
Only the **default** HTTP response body is sanitized; everything else
44+
is unchanged. A new opt-in builder flag,
45+
`create_webserver::expose_exception_messages(bool)`, restores the
46+
pre-revision behaviour of including the message in the default body.
47+
The flag is documented as development-only (see the `@warning` block
48+
on the setter).
49+
50+
**Contract points 1–6 in §5.2 stand; only the default-body clause in
51+
point 1 is amended.**
52+
53+
**Consequences (Revision 1):**
54+
- The regression tests
55+
`dr009_runtime_error_message_surfaces_in_default_body` and
56+
`dr009_non_std_exception_yields_unknown_exception_in_default_body`
57+
are renamed/split into a "fixed body" pair (the new contract) plus a
58+
"verbose body" pair (the opt-in round-trip). See
59+
`test/integ/basic.cpp`.
60+
- The legacy integration tests in `basic_suite` that pre-date this
61+
revision and assert on the message-in-body (`exception_forces_500`,
62+
`untyped_error_forces_500`, `file_serving_resource_missing`,
63+
`file_serving_resource_dir`, `long_error_message`,
64+
`dr009_feature_unavailable_lands_as_generic_500`) opt the
65+
suite-shared `ws` into the verbose mode via
66+
`.expose_exception_messages(true)` so their original assertion
67+
intent — "the dispatcher's message-forwarding path works" — is
68+
preserved without diluting coverage.
69+
- No request-id concept is introduced as part of this revision. The
70+
original action item mentioned "with the request id (if any)
71+
appended"; the codebase has no request-id plumbing today and adding
72+
one is out of scope for a security-fix revision. The fixed-body
73+
alone satisfies CWE-209. A request-id concept can be added later
74+
without re-opening this decision.
75+
- `log_dispatch_error` continues to log `e.what()` verbatim regardless
76+
of the flag. The error log is the canonical destination for the
77+
verbatim exception text. Handlers that may throw exceptions
78+
containing sensitive data (DB URLs, credentials, attacker-influenced
79+
input) SHOULD catch and sanitize the message before re-throwing if
80+
those values must not appear in the server log either; see the
81+
Doxygen note on `webserver_impl::log_dispatch_error`.
82+
- ABI: this revision is shipping on `feature/v2.0` before v2.0 cuts;
83+
SOVERSION is already 2 and there is no released ABI to preserve.
84+
The added `const bool expose_exception_messages` on `webserver` and
85+
the `bool _expose_exception_messages` on `create_webserver` recompile
86+
cleanly for any consumer TU.
87+
88+
**Related findings:** task-031 #3, task-031 #4, task-036 #44, task-034 #24.
89+
90+
---

specs/tasks/v2-deferred-backlog-plan.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -162,22 +162,24 @@ untrusted client. The decision needs a coordinated revision: the default
162162
behaviour should be sanitized; the verbose form should be opt-in.
163163

164164
**Action Items:**
165-
- [ ] Draft DR-009-rev1 in `specs/architecture/03-decisions.md` (or
166-
equivalent file): default body is a fixed string ("Internal Server
167-
Error") with the request id (if any) appended; verbose body is
168-
opt-in via a `webserver_builder.expose_exception_messages(true)` flag
169-
intended for development environments only.
170-
- [ ] Update `webserver_impl::internal_error_page` to consult the flag.
171-
- [ ] Update `log_dispatch_error` to log `e.what()` verbatim to the
165+
- [x] Draft DR-009-rev1 in `specs/architecture/11-decisions/DR-009.md`:
166+
default body is a fixed string ("Internal Server Error"); the
167+
"with the request id (if any) appended" clause was deferred (no
168+
request-id concept exists in the codebase today; documented in
169+
DR-009 Revision 1 "Consequences"). Verbose body is opt-in via
170+
`create_webserver::expose_exception_messages(bool)` flag intended
171+
for development environments only.
172+
- [x] Update `webserver_impl::internal_error_page` to consult the flag.
173+
- [x] Update `log_dispatch_error` to log `e.what()` verbatim to the
172174
server log (where it belongs) regardless of the flag; add a Doxygen
173175
note that handlers throwing exceptions with sensitive data should
174176
catch+sanitize before throwing.
175-
- [ ] Update the two regression tests that pin the current behaviour
177+
- [x] Update the two regression tests that pin the current behaviour
176178
(`dr009_runtime_error_message_surfaces_in_default_body` and sibling)
177179
to split into:
178180
- `dr009_default_body_is_fixed_string` (new contract)
179181
- `dr009_verbose_body_surfaces_message_when_opted_in`
180-
- [ ] Update README "Error handling" section + RELEASE_NOTES.md
182+
- [x] Update README "Error handling" section + RELEASE_NOTES.md
181183
"Behaviour change" note.
182184

183185
**Dependencies:**
@@ -196,6 +198,8 @@ behaviour should be sanitized; the verbose form should be opt-in.
196198
**Related Findings:** task-031 #3, task-031 #4, task-036 #44, task-034 #24
197199
**Related Decisions:** DR-009 (revised), CWE-209
198200

201+
**Status:** Done
202+
199203
---
200204

201205
## TASK-056 — Hash-DoS hardening + prefix-route disambiguation in radix tree

0 commit comments

Comments
 (0)