This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/docs by this push:
new 1789a8924d TODO-370: Harden REST debug logging against secret
disclosure (secure-by-default no-dump)
1789a8924d is described below
commit 1789a8924d86ae53433081cd8ce6b272f0e5f46d
Author: James Bognar <[email protected]>
AuthorDate: Sun Aug 16 08:47:12 2026 -0400
TODO-370: Harden REST debug logging against secret disclosure
(secure-by-default no-dump)
Request/response bodies are no longer dumped in FINEST debug output by
default.
Body dumping is gated behind the JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES
environment
variable (read-once, trim-then-parse truthy semantics, env-only, no
system-property
fallback). When unset, a suppression placeholder naming the env var is
emitted instead.
- New juneau-rest-common primitives: DebugTextSanitizer (CR/LF +
control-char
log-forging sanitization) and RestDebugBodyScrubber SPI for opt-in custom
scrubbing.
- BasicRestDebugFormatter: env-var master gate, no-dump formatBody,
isBodyRenderable
default predicate, all-values header redaction with widened
formatter-local set,
separator folding, addRedactedHeaders; instance statusLine with
maxUriLength bound.
- RestSession.finish() finish-path containment via RichLogger.
- Docs: rewrote 10.32.RestServerLoggingAndDebugging tier table; release
note added.
- Tests: DebugTextSanitizer_Test, RestDebugFinishContainment_Test,
gate-seam bridges,
and a Spring Boot integration raw-body proof (b06) alongside gate-OFF
assertions.
---
pages/release-notes/10.0.0.md | 44 ++++++-
.../topics/10.32.RestServerLoggingAndDebugging.md | 141 +++++++++++++++++----
2 files changed, 155 insertions(+), 30 deletions(-)
diff --git a/pages/release-notes/10.0.0.md b/pages/release-notes/10.0.0.md
index d2f559bcfc..27cebe93e1 100644
--- a/pages/release-notes/10.0.0.md
+++ b/pages/release-notes/10.0.0.md
@@ -476,7 +476,8 @@ for the full removed-symbol mapping.
- **One signal.** A resource's own `java.util.logging.Logger` level is the
sole control for request/response debug
capture — no annotations, no per-request headers, no separate config keys.
Verbosity is cumulative: `INFO` =
- basic, `FINE` = + headers, `FINEST` = + bodies, one `LogRecord` per request
(stable emitted level: `INFO`).
+ basic, `FINE` = + headers, `FINEST` = + bodies (only when the body-dump gate
is set — see the secure-by-default
+ entry below), one `LogRecord` per request (stable emitted level: `INFO`).
- **Per-operation granularity** falls out of ordinary JUL parent-inheritance
via a hierarchical child logger
(`resourceClass.getName() + "." + methodName`) — elevate one method to
`FINEST` without touching the rest.
- **Spring Boot propagation path.** For real Logback backends,
`logging.level.*` now propagates into JUL by default
@@ -486,21 +487,50 @@ for the full removed-symbol mapping.
(`formatBasic`/`formatHeaders`/`formatBody`) with a shipped
`BasicRestDebugFormatter` default. It's the only
public extension point; the two-phase capture pipeline underneath it is
internal. Resolution precedence:
resource-implements-the-interface > `BeanStore`-registered bean >
`BasicRestDebugFormatter`.
-- **Secure by default.** Sensitive headers (`Authorization`,
`Proxy-Authorization`, `Cookie`, `Set-Cookie`) are
- redacted at the headers tier, and captured bodies are capped at a
configurable size (default 8 KB, enforced at
- capture time) — both overridable, both on unconditionally regardless of how
a resource's logger level got
- raised.
+- **Secure by default (hardened).** `BasicRestDebugFormatter` no longer dumps
bodies just because a logger was
+ raised to `FINEST`. See the dedicated secure-by-default entry below for the
full contract.
- **Bounded body buffering.** Request/response body-caching wrappers are
installed only when the resolved logger
is loggable at `FINEST`, and buffer at most the configured cap rather than
the entire stream — an improvement
over the prior unbounded debug body caching.
- **Marshall-layer `debug`** (recursion detection + richer serialization
exceptions) is unaffected in behavior —
`RestRequest.isDebug()` still exists, now derived and read-only, `true` when
the resolved logger is
`FINE`-or-finer.
-- **Operator caveat.** Stable-`INFO` emission means DEBUG/TRACE-selected
headers and bodies flow through INFO sinks;
- keep broad DEBUG/TRACE raises scoped until payload-hardening controls are in
place.
See the new [Logging / Debugging](/docs/topics/RestServerLoggingAndDebugging)
topic page for the full model.
+### Secure-by-default REST debug hardening (`RestDebugFormatter`)
+
+`BasicRestDebugFormatter` is hardened so that raising a resource/operation
logger to `FINE`/`FINEST` can never, by
+itself, leak plaintext credentials or hand a remote client a log-forging
channel. This is an unreleased-10.0
+**hard break** in the debug formatter's output — there is no compatibility
shim. Two reusable primitives land in
+`juneau-rest-common` (a `sanitize(...)` log-forging escaper and the
`RestDebugBodyScrubber` SPI); the formatter
+changes land in `juneau-rest-server`.
+
+- **No body dumps by default.** Request/response bodies are **never** dumped
unless an operator explicitly sets the
+ `JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES` environment variable
(environment-only, no system-property fallback, read
+ once/cached, trim-then-parse truthy kill-switch semantics). With the gate
unset, `FINEST` emits a suppression
+ placeholder + byte count instead of body content. Even opted in, bodies
render only at `FINEST` and only for
+ renderable text content (via the new `isBodyRenderable(String)` predicate);
non-renderable/binary content and
+ non-identity `Content-Encoding` yield a distinct placeholder.
+- **`RestDebugBodyScrubber` SPI** (`org.apache.juneau.http`) — an optional,
gate-subordinate, fail-closed transform
+ (`String scrub(String contentType, String body)`, registered via
`bodyScrubber(...)`) that chooses scrubbed-vs-raw
+ body text once dumping is permitted; a throw or `null` result falls closed
to a placeholder, never the raw body.
+- **All header values masked against a widened set.** Every value of every
credential-bearing header (not just the
+ first) is masked with `[REDACTED]`, using a widened formatter-local set (the
shared `RedactedHeaders.DEFAULT` plus
+ `X-Auth-Token`, `X-Authorization`, `WWW-Authenticate`, `Referer`,
`Location`) matched with separator-normalized
+ folding (so `X-Auth-Token`/`X_Auth_Token`/`XAuthToken` all match).
`RedactedHeaders.DEFAULT` is unchanged. New
+ additive `addRedactedHeaders(...)` knob alongside the replacing
`redactedHeaders(...)`.
+- **Log-forging sanitization.** The request method/URI (including the
404/unrouted path), header names/values, and
+ any dumped body text are escaped (CR/LF, NEL, line/paragraph/bidi
separators, C0/C1 controls → `\r`/`\n`/`\uXXXX`;
+ TAB preserved), so a client cannot inject a newline to forge a second log
line or a fake status banner.
+- **Bounded output.** New `maxHeaders` (100), `maxHeaderScan` (1000),
`maxFieldLength` (1024), and `maxUriLength`
+ (2048) knobs bound header and URI output; the existing `bodyCap()` remains
the single body cap.
+- **Finish-path containment.** A formatter or scrubber that throws during
request completion can no longer escape
+ and fail a completed request — only a fixed diagnostic token is logged
(never the secret, message, or stack).
+
+See the [Logging /
Debugging](/docs/topics/RestServerLoggingAndDebugging#secure-by-default) topic
page for the full
+model and the operator opt-in danger callout.
+
### juneau-rest-client / juneau-rest-common
### Next-generation remote-proxy declarative features
diff --git a/pages/topics/10.32.RestServerLoggingAndDebugging.md
b/pages/topics/10.32.RestServerLoggingAndDebugging.md
index 9f4201cd0a..248d958012 100644
--- a/pages/topics/10.32.RestServerLoggingAndDebugging.md
+++ b/pages/topics/10.32.RestServerLoggingAndDebugging.md
@@ -12,11 +12,14 @@ that class's endpoints, at three cumulative tiers.
- **One signal.** No debug annotations, no on-demand per-request headers, no
separate logging SPI to wire up.
The resolved JUL logger's level is the sole control.
-- **Cumulative tiers.** `INFO` = basic request/response logging, `FINE` = adds
headers, `FINEST` = adds bodies.
- Each tier includes everything the tier below it logs.
+- **Cumulative tiers.** `INFO` = basic request/response logging, `FINE` = adds
headers, `FINEST` = adds bodies —
+ but **only** when body dumping has been explicitly opted in via the
`JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES`
+ environment variable; otherwise `FINEST` emits a suppression placeholder
instead of body content. Each tier
+ includes everything the tier below it logs.
- **One log record per request**, always emitted at JUL `INFO`. The resolved
tier controls content only.
-- **Secure by default.** Sensitive headers are redacted and bodies are capped,
both overridable — see
- [Secure by default](#secure-by-default-redaction--body-cap) below.
+- **Secure by default.** Bodies are **never** dumped unless an operator sets
an environment-variable master gate;
+ every value of every credential-bearing header is masked; every
client-controlled string is escaped against
+ log-forging; and all output is bounded — see [Secure by
default](#secure-by-default) below.
## Level resolution
@@ -103,8 +106,8 @@ makes its *operations* log full detail; it does not
retroactively add header/bod
|---|---|
| below `INFO` | Nothing emitted. |
| `INFO` | Basic request/response summary (method, path, status, timing). |
-| `FINE` | Basic **+** request/response headers. |
-| `FINEST` | Basic + headers **+** request/response bodies. |
+| `FINE` | Basic **+** request/response headers (every value of every
credential-bearing header masked). |
+| `FINEST` | Basic + headers **+** request/response body content — **only if**
`JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES` is set (and the content is renderable);
otherwise a suppression placeholder + byte count. |
`FINER` resolves through the `FINE` tier (headers), while `CONFIG` resolves
through `INFO` (basic line only), matching
JUL `isLoggable(...)` semantics used by the pipeline.
@@ -125,7 +128,9 @@ customization surface is the
[`RestDebugFormatter`](#the-restdebugformatter-spi)
happens at all, so requests below `FINEST` pay no buffering overhead.
- **Phase B (request completion):** the tier is re-derived, the resolved
`RestDebugFormatter` is invoked
cumulatively (`formatBasic` always, `+ formatHeaders` at `FINE`-or-finer, `+
formatBody` at `FINEST`), header
- redaction is applied, and exactly one `LogRecord` is emitted at `INFO`
through the resolved logger.
+ redaction and log-forging sanitization are applied, and exactly one
`LogRecord` is emitted at `INFO` through the
+ resolved logger. `formatBody` only ever emits actual body bytes when the
`JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES`
+ gate is set (see [Secure by default](#secure-by-default)); otherwise it
emits a placeholder.
- **Bounded capture.** The caching wrapper buffers at most the configured body
cap (default 8 KB), not the entire
stream, so memory use is bounded even for large uploads/downloads.
- **Edge case:** if the logger's level is changed *during* a request (between
Phase A and Phase B), the two phases
@@ -143,7 +148,9 @@ package org.apache.juneau.rest.server.logging;
public interface RestDebugFormatter {
String formatBasic(RestRequest req, RestResponse res); //
INFO tier
default String formatHeaders(RestRequest req, RestResponse res) {...} //
added at FINE
- default String formatBody(RestRequest req, RestResponse res) {...} //
added at FINEST
+ default String formatBody(RestRequest req, RestResponse res) {...} //
added at FINEST (gated)
+ default String statusLine(HttpServletRequest req, HttpServletResponse res)
{...} // sanitized + URI-capped
+ default boolean isBodyRenderable(String contentType) {...} //
text-vs-binary predicate
}
```
@@ -152,8 +159,9 @@ Override only the tier(s) you care about — the other tiers
keep the shipped de
### Default implementation
<a
href="/site/apidocs/org/apache/juneau/rest/server/logging/BasicRestDebugFormatter.html"
target="_blank">BasicRestDebugFormatter</a>
-ships working defaults for all three tiers and owns the redaction set + body
cap (see below). It's the
-zero-configuration default — you don't need to register anything to get useful
debug output.
+ships working defaults for all three tiers and owns the header-redaction set,
the body-dump gate, the
+`RestDebugBodyScrubber` hook, and the output bounds (see [Secure by
default](#secure-by-default) below). It's the
+zero-configuration default — you don't need to register anything to get
useful, secure debug output.
### Resolution
@@ -184,21 +192,105 @@ public class MyResource extends BasicRestServlet {
```
:::
-## Secure by default: redaction & body cap
+## Secure by default
-`BasicRestDebugFormatter` never logs sensitive data by accident:
+`BasicRestDebugFormatter` is designed so that raising a resource/operation
logger to `FINE`/`FINEST` can never, by
+itself, turn the log stream into a plaintext-credential dump or a log-forging
channel controlled by a remote
+client. Four independent protections apply.
-- **Header redaction** — `Authorization`, `Proxy-Authorization`, `Cookie`, and
`Set-Cookie` are masked (e.g. `***`)
- at the headers tier (`FINE`-or-finer). The redacted-header set is
configurable — replace it outright or add to
- it via the formatter's builder-style configuration hooks.
-- **Body cap** — bodies are truncated to a configurable byte cap (default **8
KB**), with a
- `…[truncated N bytes]` marker appended. The cap is enforced at *capture*
time (Phase A above), not at format
- time, so oversized bodies never sit fully buffered in memory.
+### No body dumps without an explicit opt-in
-Both the redacted-header set and the body cap are overridable, but the
defaults are deliberately conservative —
-turning on `FINE`/`FINEST` logging should never be a credential-leak footgun
by itself. See the
+Request/response **bodies are never dumped by default** — not at `FINEST`, not
for any content type. Dumping is a
+deliberate operator opt-in behind a single **environment-variable master
gate**:
+
+```bash
+export JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES=true
+```
+
+Semantics:
+
+- **Environment variable only.** There is **no** system-property fallback. It
is read **once** per JVM and cached.
+- **Trim-then-parse truthy.** The value is trimmed, then any non-empty value
enables *except* case-insensitive
+ `false` or `0`, which disable. Unset, empty, or all-whitespace disables.
This is a fail-safe kill switch:
+ `" false "` and `" "` both resolve to **disabled** and never silently
enable dumping.
+- **Gate off ⇒ placeholder only.** With the gate unset, `formatBody` emits
only a suppression placeholder plus the
+ captured byte count and content-type — never body bytes in any
representation — even if a scrubber is configured.
+- **Gate on ⇒ only at `FINEST`, only on renderable content.** Even opted in,
bodies render only at the `FINEST`
+ tier and only when the content is renderable text (see below).
Non-renderable/binary content yields a distinct
+ "not rendered" placeholder.
+
+:::danger
+Setting `JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES` re-enables plaintext
request/response body logging at `FINEST`. Treat
+`FINEST` logs as sensitive: restrict their sink, and supply a
[`RestDebugBodyScrubber`](#body-scrubbing-spi) if any
+secret-bearing bodies (tokens, PII, card data) are possible. Prefer leaving
the gate unset in production.
+:::
+
+### Renderable-content predicate
+
+`isBodyRenderable(String contentType)` decides whether a body is text worth
logging. It parses the media type
+(ignoring parameters such as `; charset=utf-8`) and treats as renderable:
`text/*`, `application/json`,
+`application/xml`, any `+json`/`+xml` suffix type, and
`application/x-www-form-urlencoded`. Everything else —
+including `multipart/form-data`, absent, or blank content types — is
**non-renderable**. A non-identity
+`Content-Encoding` (e.g. `gzip`) is also treated as non-renderable. The
decision is made per side: the request
+content type governs the request body, the response content type governs the
response body.
+
+### Body scrubbing SPI
+
+When body dumping is opted in, you can transform body text before it is logged
via the
+<a href="/site/apidocs/org/apache/juneau/http/RestDebugBodyScrubber.html"
target="_blank">RestDebugBodyScrubber</a>
+SPI (`String scrub(String contentType, String body)`), registered with
`bodyScrubber(...)`:
+
+- It is **gate-subordinate**: a scrubber is invoked **only** after the master
gate has already permitted dumping.
+ It merely chooses *scrubbed-vs-raw* text; it never causes a body to be
emitted while the gate is unset.
+- It is **fail-closed**: if `scrub(...)` throws or returns `null`, the
formatter emits a suppression placeholder
+ instead of the body — it never falls back to the raw, unscrubbed body.
+- Non-`null` output is still sanitized (control characters escaped) and
length-capped before it reaches the log.
+- Implementations must be **thread-safe** — the formatter may call
`scrub(...)` concurrently.
+
+No default scrubber ships: with the gate set and no scrubber, the raw body is
dumped (sanitized + capped).
+
+### Header redaction — all values, widened set
+
+At the headers tier (`FINE`-or-finer), **every value of every
credential-bearing header** is masked with
+`[REDACTED]` (multi-valued headers such as repeated `Set-Cookie` are masked
value-by-value, not just the first).
+The formatter matches against a **widened, formatter-local** set — a superset
of the shared
+`RedactedHeaders.DEFAULT` (`Authorization`, `Cookie`, `Set-Cookie`,
`Proxy-Authorization`, `X-API-Key`) plus
+`X-Auth-Token`, `X-Authorization`, `WWW-Authenticate`, `Referer`, and
`Location`. Matching is
+**separator-normalized**: it case-folds and strips `-`/`_`, so `X-Auth-Token`,
`X_Auth_Token`, and `XAuthToken`
+all match. The shared `RedactedHeaders.DEFAULT` itself is **left unchanged**.
+
+Configure the masked set with:
+
+- `redactedHeaders(Collection<String>)` — **replaces** the masked set.
+- `addRedactedHeaders(Collection<String>)` — **adds** to the built-in widened
set without dropping the built-ins.
+
+### Log-forging sanitization
+
+Every client-controlled string — the request method and URI (including on the
404 / unrouted path), header names
+and values, and body text (raw or scrubber-produced) — is passed through a
sanitizer that escapes CR/LF and other
+control characters (NEL, line/paragraph separators, bidi controls, C0/C1
controls; TAB is preserved) to visible,
+inert forms (`\r`, `\n`, `\uXXXX`). A remote client therefore cannot inject a
newline to forge a second physical
+log line or a fake status banner. The order applied to every emitted string is
**mask → escape → cap**.
+
+### Bounded output
+
+All output axes are bounded so a flood cannot exhaust the log sink:
+
+| Bound | Knob | Default |
+|---|---|---|
+| Captured body bytes (Phase A capture time), then `…[truncated N bytes]` |
`bodyCap(int)` | 8 KB |
+| Emitted header values per block, then `…[+K more headers omitted]` |
`maxHeaders(int)` | 100 |
+| Header values scanned while counting omissions | `maxHeaderScan(int)` | 1000
|
+| Per header name/value length (after masking/escaping) |
`maxFieldLength(int)` | 1024 |
+| Rendered URI length | `maxUriLength(int)` | 2048 |
+
+The body byte cap is enforced at *capture* time (Phase A above), so oversized
bodies never sit fully buffered in
+memory; a second character-level cap is applied after sanitization so a
scrubber that returns a huge string is
+still bounded. There is no separate body-dump cap knob — `bodyCap()` is the
single source of truth.
+
+See the
[migration guide's footgun
callout](/docs/topics/V10MigrationGuide#rest-debuglogging-redesign-jul-level-driven-restdebugformatter)
-for the one case where this still matters operationally.
+for operational guidance on raising logger levels in production.
## Interaction with marshall-layer `debug`
@@ -247,8 +339,11 @@ introducing a separate boolean debug flag.
- Plain JUL / other SLF4J backends: configure JUL directly.
:::danger
-Stable-INFO emission means DEBUG/TRACE-selected headers and bodies now travel
through INFO sinks. Before dedicated
-payload-hardening controls are enabled, avoid broad DEBUG/TRACE raises on
production resource loggers.
+Stable-INFO emission means `FINE`/`FINEST`-selected detail travels through
INFO sinks. Headers are always masked
+and bodies are suppressed by default (see [Secure by
default](#secure-by-default)), but a broad `FINEST` raise on a
+production resource logger still emits full header names and metadata through
INFO — and, if
+`JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES` is set, plaintext bodies. Raise levels
deliberately and keep the body-dump
+gate unset in production.
:::
## Migrating from pre-10.0