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

Reply via email to