Skip to content

Commit b0f09cd

Browse files
stevenvegtreinkrul
andauthored
fix(http): fall back to metadata logging in strictmode (#4440)
* fix(http): fall back to metadata logging in strictmode http.log=metadata-and-body logs full request and response bodies at Info severity. The loggable content types include application/json and application/x-www-form-urlencoded, which are exactly the OAuth token endpoint's request and response types, so client assertions, VP tokens, authorization codes and issued access tokens all reach the log in full. In strictmode the value is now reset to metadata with a warning at startup, following the existing warn-and-override precedent for unsafe-but-valid configuration values (auth.accesstokenlifespan, cpuprofile). Startup is not failed: remediation for the operator is removing a single config value, and a security patch must be safe to apply without reading release notes. Assisted-by: AI * docs: add release note for strictmode body logging fallback Assisted-by: AI --------- Co-authored-by: Rein Krul <info@reinkrul.nl>
1 parent ae207e7 commit b0f09cd

6 files changed

Lines changed: 127 additions & 74 deletions

File tree

docs/pages/deployment/configuration.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ As a general safety precaution ``auth.contractvalidators`` ignores the ``dummy``
9393
requesting an access token from another node on ``/n2n/auth/v1/accesstoken`` does not return any error details,
9494
``auth.accesstokenlifespan`` is always 60 seconds,
9595
json-ld context can only be downloaded from trusted domains configured in ``jsonld.contexts.remoteallowlist``,
96+
``http.log=metadata-and-body`` is not allowed and is changed to ``metadata`` at startup (request and response bodies on the OAuth endpoints contain credentials, which must not be written to logs),
9697
and the ``internalratelimiter`` is always on.
9798

9899
Interacting with remote Nuts nodes requires HTTPS: it will refuse to connect to plain HTTP endpoints when in strict mode.

docs/pages/deployment/server_options.rst

Lines changed: 72 additions & 72 deletions
Large diffs are not rendered by default.

docs/pages/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Unreleased
1414
* #4233: ``request-credential`` API gains an optional ``credential_request_params`` JSON object overlaid on top of the OpenID4VCI Credential Request body sent to the issuer. Lets the wallet talk to issuers that accept additional fields, or to override the credential request entirely.
1515

1616
## Security
17+
* #4440: In strictmode, ``http.log: metadata-and-body`` is no longer honored: the node resets it to ``metadata`` at startup and logs a warning. Full body logging wrote OAuth token endpoint request and response bodies (client assertions, VP tokens, authorization codes and issued access tokens) to the log at Info severity. Non-strictmode deployments are unaffected. By @stevenvegt in https://github.com/nuts-foundation/nuts-node/pull/4440
1718
* Upgrade Go to 1.26.5 to address `GO-2026-5856 <https://pkg.go.dev/vuln/GO-2026-5856>`_ (de-anonymization of Encrypted Client Hello (ECH) handshakes), `GO-2026-5039 <https://pkg.go.dev/vuln/GO-2026-5039>`_ (net/textproto included user input in error messages, allowing injection of misleading content into error logs) and `GO-2026-5037 <https://pkg.go.dev/vuln/GO-2026-5037>`_ (quadratic time complexity in crypto/x509 certificate hostname verification).
1819
* #4421: Stop reflecting fetched HTTP response bodies in API responses. The OAuth2 and OpenID4VCI callback handlers no longer place a remote endpoint's response body or error text into the returned ``error_description``, the did:web resolver no longer returns the fetched document body in its parse error, and the Discovery Service client no longer includes the remote server's error response in errors returned through the discovery APIs. Such content is now logged (truncated) for diagnostics instead. Static context such as the endpoint that failed is retained. By @stevenvegt in https://github.com/nuts-foundation/nuts-node/pull/4421
1920
* #4244: Centralize outbound URL validation (HTTPS-only, no RFC 2606 reserved hosts) in the shared strict-mode HTTP client instead of duplicating it in each caller (OpenID4VCI, IAM, OAuth relying-party). The same check now also runs on every redirect target, not just the first request. IP-address validation is left to the dial-time SSRF guard (#4420), which already honors ``http.client.allowedinternalcidrs``/``deniedcidrs``. By @JorisHeadease in https://github.com/nuts-foundation/nuts-node/pull/4246

http/cmd/cmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func FlagSet() *pflag.FlagSet {
3434
flags.String("http.internal.auth.type", string(defs.Internal.Auth.Type), fmt.Sprintf("Whether to enable authentication for /internal endpoints, specify '%s' for bearer token mode or '%s' for legacy bearer token mode.", http.BearerTokenAuthV2, http.BearerTokenAuth))
3535
flags.String("http.internal.auth.audience", defs.Internal.Auth.Audience, "Expected audience for JWT tokens (default: hostname)")
3636
flags.String("http.internal.auth.authorizedkeyspath", defs.Internal.Auth.AuthorizedKeysPath, "Path to an authorized_keys file for trusted JWT signers")
37-
flags.String("http.log", string(defs.Log), fmt.Sprintf("What to log about HTTP requests. Options are '%s', '%s' (log request method, URI, IP and response code), and '%s' (log the request and response body, in addition to the metadata). When debug vebosity is set the authorization headers are also logged when the request is fully logged.", http.LogNothingLevel, http.LogMetadataLevel, http.LogMetadataAndBodyLevel))
37+
flags.String("http.log", string(defs.Log), fmt.Sprintf("What to log about HTTP requests. Options are '%s', '%s' (log request method, URI, IP and response code), and '%s' (log the request and response body, in addition to the metadata). In strictmode, '%s' is not allowed and is changed to '%s' at startup. When debug vebosity is set the authorization headers are also logged when the request is fully logged.", http.LogNothingLevel, http.LogMetadataLevel, http.LogMetadataAndBodyLevel, http.LogMetadataAndBodyLevel, http.LogMetadataLevel))
3838
flags.String("http.clientipheader", defs.ClientIPHeaderName, "Case-sensitive HTTP Header that contains the client IP used for audit logs. For the X-Forwarded-For header only link-local, loopback, and private IPs are excluded. Switch to X-Real-IP or a custom header if you see your own proxy/infra in the logs.")
3939
flags.Int("http.cache.maxbytes", defs.ResponseCacheSize, "HTTP client maximum size of the response cache in bytes. If 0, the HTTP client does not cache responses.")
4040
flags.StringSlice("http.client.allowedinternalcidrs", defs.Client.AllowedInternalCIDRs, "IP ranges (CIDR notation, e.g. 10.0.0.0/8) exempted from the strict-mode SSRF guard, which otherwise blocks outbound requests to non-public networks. Use to permit internal flows that legitimately target a private address, such as an internal credential offering or an internal OAuth user flow. Leave empty to block all non-public addresses.")

http/engine.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ func (h *Engine) Configure(serverConfig core.ServerConfig) error {
9494
return err
9595
}
9696

97+
if serverConfig.Strictmode && h.config.Log == LogMetadataAndBodyLevel {
98+
// Request/response bodies contain credentials: the OAuth token endpoint's client_assertion,
99+
// VP tokens, authorization codes and issued access tokens.
100+
log.Logger().Warn("Body logging (http.log=metadata-and-body) is not allowed in strictmode, falling back to metadata")
101+
h.config.Log = LogMetadataLevel
102+
}
103+
97104
h.applyTracingMiddleware(h.server)
98105
h.applyRateLimiterMiddleware(h.server, serverConfig)
99106
h.applyLoggerMiddleware(h.server, []string{MetricsPath, StatusPath, HealthPath}, h.config.Log)

http/engine_test.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,10 @@ func TestEngine_LoggingMiddleware(t *testing.T) {
365365
engine := New(noop, nil)
366366
engine.config = createTestConfig()
367367
engine.config.Log = LogMetadataAndBodyLevel
368+
serverConfig := core.NewServerConfig()
369+
serverConfig.Strictmode = false
368370

369-
err := engine.Configure(*core.NewServerConfig())
371+
err := engine.Configure(*serverConfig)
370372
require.NoError(t, err)
371373
engine.Router().POST("/", func(c echo.Context) error {
372374
return c.JSON(200, "hello, world")
@@ -385,6 +387,48 @@ func TestEngine_LoggingMiddleware(t *testing.T) {
385387
assert.Contains(t, output.String(), `HTTP response body: \"hello, world\"`)
386388
})
387389
})
390+
t.Run("bodyLogger is disabled in strict mode", func(t *testing.T) {
391+
// Configure sets the package-global client strict mode flag; restore it afterwards.
392+
oldStrictMode := client.StrictMode
393+
t.Cleanup(func() { client.StrictMode = oldStrictMode })
394+
warnOutput := new(bytes.Buffer)
395+
logrus.StandardLogger().AddHook(&writer.Hook{
396+
Writer: warnOutput,
397+
LogLevels: []logrus.Level{logrus.WarnLevel},
398+
})
399+
engine := New(noop, nil)
400+
engine.config = createTestConfig()
401+
engine.config.Log = LogMetadataAndBodyLevel
402+
serverConfig := core.NewServerConfig()
403+
serverConfig.Strictmode = true
404+
405+
err := engine.Configure(*serverConfig)
406+
require.NoError(t, err)
407+
engine.Router().POST("/", func(c echo.Context) error {
408+
return c.JSON(200, "very-secret-response")
409+
})
410+
411+
err = engine.Start()
412+
require.NoError(t, err)
413+
defer engine.Shutdown()
414+
415+
assertServerStarted(t, engine.config.Internal.Address)
416+
417+
output.Reset()
418+
response, err := http.Post("http://"+engine.config.Public.Address, "application/json", bytes.NewReader([]byte(`{"assertion": "very-secret-request"}`)))
419+
require.NoError(t, err)
420+
require.Equal(t, http.StatusOK, response.StatusCode)
421+
422+
logs := output.String()
423+
// Metadata logging must still work: a request log entry with the response status.
424+
assert.Contains(t, logs, `msg="HTTP request"`)
425+
assert.Contains(t, logs, "status=200")
426+
// Neither the request nor the response body content may reach the log.
427+
assert.NotContains(t, logs, "very-secret-request")
428+
assert.NotContains(t, logs, "very-secret-response")
429+
assert.Equal(t, LogLevel(LogMetadataLevel), engine.config.Log)
430+
assert.Contains(t, warnOutput.String(), "Body logging (http.log=metadata-and-body) is not allowed in strictmode, falling back to metadata")
431+
})
388432
}
389433

390434
func assertServerStarted(t *testing.T, address string) {

0 commit comments

Comments
 (0)