Repository: langchain4j/langchain4j (2026-07-19T12:05:39Z)

Issue:
[BUG] AwsLoggingInterceptor logs Authorization and X-Amz-Security-Token headers when request/response logging is enabled
**Describe the bug**
When `logRequests(true)` / `logResponses(true)` is enabled on a Bedrock model, `AwsLoggingInterceptor` logs the full HTTP header map. Since `beforeTransmission` runs *after* SigV4 signing, the log output includes the `Authorization` header (access key ID + signature) and  when temporary/STS credentials are used (assumed roles, EC2/ECS/EKS instance roles, IAM Identity Center)  the `X-Amz-Security-Token` header, which is a live, replayable session token. These credentials get written to whatever log sink the application has configured. (CWE-532: Insertion of Sensitive Information into Log File.)

**Log and Stack trace**
Current output (values shortened/fake):
```
Request:
- method: POST
- url: https://bedrock-runtime.us-east-1.amazonaws.com/...
- headers: {Authorization=[AWS4-HMAC-SHA256 Credential=AKIAFAKEEXAMPLE/..., Signature=abc123...], X-Amz-Security-Token=[IQoJb3JpZ2luX2VjFAKE...], Content-Type=[application/json], ...}
```

**To Reproduce**
```java
BedrockChatModel model = BedrockChatModel.builder()
        .modelId("...")
        .logRequests(true)
        .build();
// enable DEBUG logging for dev.langchain4j.model.bedrock.AwsLoggingInterceptor
// -> Authorization and X-Amz-Security-Token values appear in the log
```

**Expected behavior**
The values of `Authorization` and `X-Amz-Security-Token` should be redacted in log output, consistent with the header masking already done in core's `HttpRequestLogger` (and in the cohere/jina/ovh-ai/nomic request logging interceptors), and with the AWS SDK's own wire logging, which never emits these values. All other headers, URL, query params, and body should be logged unchanged.

**Please complete the following information:**
- LangChain4j version: main (current)
- LLM(s) used: Amazon Bedrock
- Java version: 17
- Spring Boot version (if applicable): N/A

**Additional context**
I have a small fix ready (masks these two header values in `AwsLoggingInterceptor`, case-insensitive, with unit tests, no API/behavior change) — will open a PR referencing this issue.


Discussion:

Patch:
diff --git a/langchain4j-bedrock/src/main/java/dev/langchain4j/model/bedrock/AwsLoggingInterceptor.java b/langchain4j-bedrock/src/main/java/dev/langchain4j/model/bedrock/AwsLoggingInterceptor.java
index 066cec11fdb..c2f1538d054 100644
--- a/langchain4j-bedrock/src/main/java/dev/langchain4j/model/bedrock/AwsLoggingInterceptor.java
+++ b/langchain4j-bedrock/src/main/java/dev/langchain4j/model/bedrock/AwsLoggingInterceptor.java
@@ -4,13 +4,17 @@
 import static java.util.Objects.isNull;
 import static java.util.Objects.nonNull;
 
+import dev.langchain4j.Internal;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
 import java.util.Optional;
-
-import dev.langchain4j.Internal;
+import java.util.Set;
+import java.util.stream.Collectors;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import software.amazon.awssdk.core.interceptor.Context;
@@ -28,6 +32,11 @@ class AwsLoggingInterceptor implements ExecutionInterceptor {
 
     private static final Logger DEFAULT_LOGGER = LoggerFactory.getLogger(AwsLoggingInterceptor.class);
 
+    /**
+     * Authentication headers whose values must never be written to logs. Compared case-insensitively.
+     */
+    private static final Set<String> SENSITIVE_HEADERS = Set.of("authorization", "x-amz-security-token");
+
     private final boolean logRequests;
     private final boolean logResponses;
     private final Logger logger;
@@ -51,7 +60,8 @@ public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttr
         if (logRequests) {
             if (request.method() == SdkHttpMethod.POST && request instanceof SdkHttpFullRequest sdkHttpFullRequest) {
                 try {
-                    ContentStreamProvider csp = sdkHttpFullRequest.contentStreamProvider().orElse(null);
+                    ContentStreamProvider csp =
+                            sdkHttpFullRequest.contentStreamProvider().orElse(null);
                     if (nonNull(csp)) body = IoUtils.toUtf8String(csp.newStream());
                 } catch (IOException e) {
                     logger.warn("Unable to obtain request body", e);
@@ -61,10 +71,9 @@ public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttr
                     "Request:\n- method: {}\n- url: {}\n- headers: {}\n- query parameters: {}\n- body: {}",
                     request.method(),
                     request.getUri(),
-                    request.headers(),
+                    maskHeaders(request.headers()),
                     request.rawQueryParameters(),
-                    body
-            );
+                    body);
         }
     }
 
@@ -80,7 +89,7 @@ private void logResponse(SdkHttpResponse response, Context.ModifyResponse contex
             logger.debug(
                     "Response Status: {} \nHeaders: {} \nResponse Body Type: {}",
                     response.statusCode(),
-                    response.headers(),
+                    maskHeaders(response.headers()),
                     context.response().getClass().getSimpleName());
         }
     }
@@ -100,4 +109,24 @@ public Optional<InputStream> modifyHttpResponseContent(
         }
         return isNull(content) ? Optional.empty() : Optional.of(new ByteArrayInputStream(content));
     }
+
+    /**
+     * Renders HTTP headers for logging, replacing the values of sensitive authentication headers
+     * (e.g. {@code Authorization}, {@code X-Amz-Security-Token}) with a placeholder so that
+     * credentials such as SigV4 signatures and temporary session tokens are not written to logs.
+     * Header-name matching is case-insensitive; all other headers are rendered unchanged.
+     */
+    static String maskHeaders(Map<String, List<String>> headers) {
+        if (headers == null || headers.isEmpty()) {
+            return "{}";
+        }
+        return headers.entrySet().stream()
+                .map(entry -> {
+                    if (SENSITIVE_HEADERS.contains(entry.getKey().toLowerCase(Locale.ROOT))) {
+                        return entry.getKey() + "=[REDACTED]";
+                    }
+                    return entry.getKey() + "=" + entry.getValue();
+                })
+                .collect(Collectors.joining(", ", "{", "}"));
+    }
 }


Test patch:
diff --git a/langchain4j-bedrock/src/test/java/dev/langchain4j/model/bedrock/AwsLoggingInterceptorTest.java b/langchain4j-bedrock/src/test/java/dev/langchain4j/model/bedrock/AwsLoggingInterceptorTest.java
new file mode 100644
index 00000000000..4e6d53615b6
--- /dev/null
+++ b/langchain4j-bedrock/src/test/java/dev/langchain4j/model/bedrock/AwsLoggingInterceptorTest.java
@@ -0,0 +1,82 @@
+package dev.langchain4j.model.bedrock;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class AwsLoggingInterceptorTest {
+
+    @Test
+    void should_redact_authorization_header() {
+        Map<String, List<String>> headers = new LinkedHashMap<>();
+        headers.put(
+                "Authorization",
+                List.of("AWS4-HMAC-SHA256 Credential=AKIAEXAMPLE/20240101/us-east-1/bedrock/aws4_request, "
+                        + "SignedHeaders=host;x-amz-date, Signature=deadbeefdeadbeef"));
+        headers.put("Content-Type", List.of("application/json"));
+
+        String result = AwsLoggingInterceptor.maskHeaders(headers);
+
+        assertThat(result).contains("Authorization=[REDACTED]");
+        assertThat(result).doesNotContain("AKIAEXAMPLE");
+        assertThat(result).doesNotContain("deadbeefdeadbeef");
+        assertThat(result).contains("Content-Type=[application/json]");
+    }
+
+    @Test
+    void should_redact_session_token_case_insensitively() {
+        Map<String, List<String>> headers = new LinkedHashMap<>();
+        headers.put("x-amz-security-token", List.of("FwoGZXIvYXdzEXAMPLESESSIONTOKEN"));
+
+        String result = AwsLoggingInterceptor.maskHeaders(headers);
+
+        assertThat(result).contains("x-amz-security-token=[REDACTED]");
+        assertThat(result).doesNotContain("EXAMPLESESSIONTOKEN");
+    }
+
+    @Test
+    void should_redact_authorization_regardless_of_case() {
+        Map<String, List<String>> headers = new LinkedHashMap<>();
+        headers.put("AUTHORIZATION", List.of("super-secret-value"));
+
+        String result = AwsLoggingInterceptor.maskHeaders(headers);
+
+        assertThat(result).contains("AUTHORIZATION=[REDACTED]");
+        assertThat(result).doesNotContain("super-secret-value");
+    }
+
+    @Test
+    void should_preserve_non_sensitive_headers() {
+        Map<String, List<String>> headers = new LinkedHashMap<>();
+        headers.put("X-Amz-Date", List.of("20240101T000000Z"));
+        headers.put("Host", List.of("bedrock-runtime.us-east-1.amazonaws.com"));
+
+        String result = AwsLoggingInterceptor.maskHeaders(headers);
+
+        assertThat(result).contains("X-Amz-Date=[20240101T000000Z]");
+        assertThat(result).contains("Host=[bedrock-runtime.us-east-1.amazonaws.com]");
+    }
+
+    @Test
+    void should_preserve_multiple_values_for_non_sensitive_headers() {
+        Map<String, List<String>> headers = new LinkedHashMap<>();
+        headers.put("Accept", List.of("application/json", "text/event-stream"));
+
+        String result = AwsLoggingInterceptor.maskHeaders(headers);
+
+        assertThat(result).contains("Accept=[application/json, text/event-stream]");
+    }
+
+    @Test
+    void should_handle_empty_map() {
+        assertThat(AwsLoggingInterceptor.maskHeaders(Map.of())).isEqualTo("{}");
+    }
+
+    @Test
+    void should_handle_null_map() {
+        assertThat(AwsLoggingInterceptor.maskHeaders(null)).isEqualTo("{}");
+    }
+}