diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b5ae1e..a6e04a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ## [5.4.21] 2026-04-21 +### Added +- Added redaction of sensitive HTTP header values in debug logging by default, + plus the `com.oracle.nosql.sdk.nosqldriver.log-sensitive-headers` system + property to allow full header values when needed for debugging. + ### Changed - Updated netty version to 4.1.132.Final diff --git a/driver/src/main/java/oracle/nosql/driver/httpclient/ResponseHandler.java b/driver/src/main/java/oracle/nosql/driver/httpclient/ResponseHandler.java index 6b49d4c3..b1968482 100644 --- a/driver/src/main/java/oracle/nosql/driver/httpclient/ResponseHandler.java +++ b/driver/src/main/java/oracle/nosql/driver/httpclient/ResponseHandler.java @@ -8,6 +8,7 @@ package oracle.nosql.driver.httpclient; import static oracle.nosql.driver.util.LogUtil.logFine; +import static oracle.nosql.driver.util.LogUtil.logHeaders; import static oracle.nosql.driver.util.HttpConstants.REQUEST_ID_HEADER; import java.io.Closeable; @@ -203,8 +204,8 @@ void receive(RequestState requestState) { ", but got response for request " + resReqId + ": discarding response"); if (resReqId == null) { - logFine(logger, "Headers for discarded response: " + - requestState.getHeaders()); + logHeaders(logger, "Headers for discarded response", + requestState.getHeaders()); if (this.allowRetry) { this.cause = new ProtocolException( "Received invalid response with no requestId"); diff --git a/driver/src/main/java/oracle/nosql/driver/iam/FederationRequestHelper.java b/driver/src/main/java/oracle/nosql/driver/iam/FederationRequestHelper.java index 18203c3b..a15a57be 100644 --- a/driver/src/main/java/oracle/nosql/driver/iam/FederationRequestHelper.java +++ b/driver/src/main/java/oracle/nosql/driver/iam/FederationRequestHelper.java @@ -9,6 +9,7 @@ import static oracle.nosql.driver.iam.Utils.*; import static oracle.nosql.driver.util.HttpConstants.*; +import static oracle.nosql.driver.util.LogUtil.logHeaderValue; import java.io.IOException; import java.io.StringWriter; @@ -173,8 +174,10 @@ static HttpHeaders setHeaders(URI uri, SINGATURE_VERSION); - logTrace(logger, "Resource Principal Token request" + - " authorization header " + authHeader); + logHeaderValue(logger, + "Resource Principal Token request authorization header", + AUTHORIZATION, + authHeader); HttpHeaders headers = new DefaultHttpHeaders(); return headers .set(DATE, date) @@ -217,8 +220,10 @@ static HttpHeaders setHeaders(URI uri, signature, SINGATURE_VERSION); - logTrace(logger, "Federation request authorization header " + - authHeader); + logHeaderValue(logger, + "Federation request authorization header", + AUTHORIZATION, + authHeader); HttpHeaders headers = new DefaultHttpHeaders(); return headers .set(DATE, date) diff --git a/driver/src/main/java/oracle/nosql/driver/util/HttpRequestUtil.java b/driver/src/main/java/oracle/nosql/driver/util/HttpRequestUtil.java index ce1cd705..0ad0a1ba 100644 --- a/driver/src/main/java/oracle/nosql/driver/util/HttpRequestUtil.java +++ b/driver/src/main/java/oracle/nosql/driver/util/HttpRequestUtil.java @@ -14,6 +14,8 @@ import static io.netty.handler.codec.http.HttpMethod.POST; import static io.netty.handler.codec.http.HttpMethod.PUT; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import static oracle.nosql.driver.util.LogUtil.formatHeadersForLog; +import static oracle.nosql.driver.util.LogUtil.logHeaders; import static oracle.nosql.driver.util.LogUtil.logFine; import static oracle.nosql.driver.util.LogUtil.logInfo; import static oracle.nosql.driver.util.HttpConstants.CONTENT_LENGTH; @@ -231,7 +233,7 @@ private static HttpResponse doRequest(HttpClient httpClient, uri, headers, method, payload, channel); } addRequiredHeaders(request); - logFine(logger, request.headers().toString()); + logHeaders(logger, "Request headers", request.headers()); httpClient.runRequest(request, responseHandler, channel); if (responseHandler.await(timeoutMs)) { throw new TimeoutException("Request timed out after " + @@ -415,7 +417,7 @@ public HttpHeaders getHeaders() { public String toString() { return "HttpResponse [statusCode=" + statusCode + "," + "output=" + output + "," + "headers=" + - (headers == null ? "null" : headers.toString()) + "]"; + formatHeadersForLog(headers) + "]"; } } } diff --git a/driver/src/main/java/oracle/nosql/driver/util/LogUtil.java b/driver/src/main/java/oracle/nosql/driver/util/LogUtil.java index 972493e7..694aab96 100644 --- a/driver/src/main/java/oracle/nosql/driver/util/LogUtil.java +++ b/driver/src/main/java/oracle/nosql/driver/util/LogUtil.java @@ -7,14 +7,33 @@ package oracle.nosql.driver.util; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpHeaders; + /** * Utility methods to facilitate Logging. */ public class LogUtil { + private static final String REDACTED = ""; + private static final String LOG_SENSITIVE_HEADERS_PROPERTY = + "com.oracle.nosql.sdk.nosqldriver.log-sensitive-headers"; + private static final Set SENSITIVE_HEADERS = new HashSet<>( + Arrays.asList(HttpConstants.AUTHORIZATION.toLowerCase(Locale.ROOT), + "proxy-authorization", + HttpConstants.COOKIE.toLowerCase(Locale.ROOT), + "set-cookie", + "opc-obo-token", + "security-context")); + public static boolean isFineEnabled(Logger logger) { return logger != null && logger.isLoggable(Level.FINE); } @@ -67,4 +86,97 @@ public static void logTrace(Logger logger, String msg) { public static boolean isLoggable(Logger logger, Level level) { return (logger != null && logger.isLoggable(level)); } + + public static boolean isSensitiveHeaderLoggingEnabled(Logger logger) { + if (!isFineEnabled(logger)) { + return false; + } + try { + return Boolean.getBoolean(LOG_SENSITIVE_HEADERS_PROPERTY); + } catch (SecurityException se) { + return false; + } + } + + public static void logHeaders(Logger logger, + String label, + HttpHeaders headers) { + if (isFineEnabled(logger)) { + logger.log(Level.FINE, + label + ": " + + formatHeadersForLog( + headers, + !isSensitiveHeaderLoggingEnabled(logger))); + } + } + + public static void logHeaderValue(Logger logger, + String label, + String headerName, + String value) { + if (isFineEnabled(logger)) { + logger.log(Level.FINE, + label + ": " + + formatHeaderValueForLog( + headerName, + value, + !isSensitiveHeaderLoggingEnabled(logger))); + } + } + + public static String formatHeadersForLog(HttpHeaders headers) { + return formatHeadersForLog(headers, true); + } + + public static String formatHeadersForLog(HttpHeaders headers, + boolean redactSensitive) { + if (headers == null) { + return "null"; + } + + final HttpHeaders formatted = new DefaultHttpHeaders(false); + for (Map.Entry entry : headers) { + formatted.add(entry.getKey(), + formatHeaderValueForLog(entry.getKey(), + entry.getValue(), + redactSensitive)); + } + return formatted.toString(); + } + + public static String formatHeaderValueForLog(String headerName, + String value) { + return formatHeaderValueForLog(headerName, value, true); + } + + public static String formatHeaderValueForLog(String headerName, + String value, + boolean redactSensitive) { + if (!redactSensitive || !isSensitiveHeader(headerName)) { + return value; + } + return redactHeaderValue(headerName, value); + } + + public static String redactHeaderValue(String headerName, String value) { + if (value == null) { + return REDACTED; + } + final String lowerName = headerName.toLowerCase(Locale.ROOT); + if (HttpConstants.AUTHORIZATION.toLowerCase(Locale.ROOT) + .equals(lowerName) || + "proxy-authorization".equals(lowerName)) { + final int space = value.indexOf(' '); + if (space > 0) { + return value.substring(0, space + 1) + REDACTED; + } + } + return REDACTED; + } + + private static boolean isSensitiveHeader(String headerName) { + return headerName != null && + SENSITIVE_HEADERS.contains( + headerName.toLowerCase(Locale.ROOT)); + } } diff --git a/driver/src/test/java/oracle/nosql/driver/util/LogUtilTest.java b/driver/src/test/java/oracle/nosql/driver/util/LogUtilTest.java new file mode 100644 index 00000000..baa21967 --- /dev/null +++ b/driver/src/test/java/oracle/nosql/driver/util/LogUtilTest.java @@ -0,0 +1,92 @@ +/*- + * Copyright (c) 2011, 2026 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package oracle.nosql.driver.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpHeaders; + +public class LogUtilTest { + + @Test + public void testRedactHeaderValue() { + assertEquals("Bearer ", + LogUtil.redactHeaderValue(HttpConstants.AUTHORIZATION, + "Bearer secret-token")); + assertEquals("Basic ", + LogUtil.redactHeaderValue("Proxy-Authorization", + "Basic dXNlcjpwYXNz")); + assertEquals("", + LogUtil.redactHeaderValue("opc-obo-token", + "delegation-token")); + assertEquals("visible", + LogUtil.formatHeaderValueForLog("X-Custom", "visible")); + } + + @Test + public void testFormatHeadersForLogRedactsSensitiveHeaders() { + final HttpHeaders headers = new DefaultHttpHeaders(); + headers.add(HttpConstants.AUTHORIZATION, "Bearer secret-token"); + headers.add(HttpConstants.COOKIE, "session=abc123"); + headers.add("opc-obo-token", "delegation-token"); + headers.add("security-context", "opaque-security-context"); + headers.add("X-Custom", "visible"); + + final String formatted = LogUtil.formatHeadersForLog(headers); + + assertTrue(formatted.contains("Authorization: Bearer ")); + assertTrue(formatted.contains("Cookie: ")); + assertTrue(formatted.contains("opc-obo-token: ")); + assertTrue(formatted.contains("security-context: ")); + assertTrue(formatted.contains("X-Custom: visible")); + assertFalse(formatted.contains("secret-token")); + assertFalse(formatted.contains("abc123")); + assertFalse(formatted.contains("delegation-token")); + assertFalse(formatted.contains("opaque-security-context")); + } + + @Test + public void testFormatHeadersForLogCanKeepSensitiveHeaders() { + final HttpHeaders headers = new DefaultHttpHeaders(); + headers.add(HttpConstants.AUTHORIZATION, "Bearer secret-token"); + headers.add(HttpConstants.COOKIE, "session=abc123"); + + final String formatted = LogUtil.formatHeadersForLog(headers, false); + + assertTrue(formatted.contains("Authorization: Bearer secret-token")); + assertTrue(formatted.contains("Cookie: session=abc123")); + } + + @Test + public void testFormatHeadersForLogPreservesNonValidatingBehavior() { + final HttpHeaders headers = new DefaultHttpHeaders(false); + headers.add("Bad Header", "visible"); + headers.add(HttpConstants.AUTHORIZATION, "Bearer secret-token"); + + final String formatted = LogUtil.formatHeadersForLog(headers); + + assertTrue(formatted.contains("Bad Header: visible")); + assertTrue(formatted.contains("Authorization: Bearer ")); + } + + @Test + public void testFormatHeaderValueForLogCanSkipRedaction() { + assertEquals("", + LogUtil.formatHeaderValueForLog(HttpConstants.COOKIE, + "session=abc123")); + assertEquals("session=abc123", + LogUtil.formatHeaderValueForLog(HttpConstants.COOKIE, + "session=abc123", + false)); + } +}