Redact sensitive AWS auth headers (Authorization, X-Amz-Security-Toke… (#5799)

## Issue

Closes #5798

## Change

When `logRequests`/`logResponses` are enabled, `AwsLoggingInterceptor`
logs the full HTTP header set. Because `beforeTransmission` runs after
SigV4 signing, this 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` a live, replayable session token writing
credentials to whatever log sink is configured (CWE-532).

This PR masks the values of these two headers (case-insensitive) with
`[REDACTED]` before logging, via a small package-private `maskHeaders()`
helper. This matches the header redaction already used elsewhere in
langchain4j (core `HttpRequestLogger`, and the cohere/jina/ovh-ai/nomic
request logging interceptors) and the AWS SDK's own wire logging, which
never emits these values.

All other headers, URL, query parameters, and body are logged unchanged.
No public API or request-behavior change only debug log text. Adds
`AwsLoggingInterceptorTest` (7 unit tests covering redaction of both
headers, case variants, non-sensitive header preservation, multi-value
headers, and null/empty maps).

Note: the import re-sort in the diff is mandated by the repo's spotless
config (matches sibling `BedrockCustomHeadersInterceptor`); no other
reformatting was done. `spotless:check` passes.

## General checklist

<!-- Please double-check the following points and mark them like this:
[X] -->

- [X] There are no breaking changes (API, behaviour)
- [X] I have added unit and/or integration tests for my change
- [X] The tests cover both positive and negative cases
- [X] I have manually run all the unit and integration tests in the
module I have added/changed, and they are all green
- [ ] I have manually run all the unit and integration tests in the
[core](https://github.com/langchain4j/langchain4j/tree/main/langchain4j-core)
and
[main](https://github.com/langchain4j/langchain4j/tree/main/langchain4j)
modules, and they are all green


## Checklist for adding new maven module

<!-- Please double-check the following points and mark them like this:
[X] -->

- [ ] I have added my new module in the root `pom.xml` and
`langchain4j-bom/pom.xml`

## Checklist for adding new embedding store integration

<!-- Please double-check the following points and mark them like this:
[X] -->

- [ ] I have added a `{NameOfIntegration}EmbeddingStoreIT` that extends
from either `EmbeddingStoreIT` or `EmbeddingStoreWithFilteringIT`
- [ ] I have added a `{NameOfIntegration}EmbeddingStoreRemovalIT` that
extends from `EmbeddingStoreWithRemovalIT`

## Checklist for changing existing embedding store integration

<!-- Please double-check the following points and mark them like this:
[X] -->

- [ ] I have manually verified that the
`{NameOfIntegration}EmbeddingStore` works correctly with the data
persisted using the latest released version of LangChain4j
This commit is contained in:
Omkar Kathile 2026-07-20 12:40:03 +05:30 committed by GitHub
parent 73ec8a22cb
commit c27e6e9c2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 118 additions and 7 deletions

View File

@ -4,13 +4,17 @@ import static dev.langchain4j.internal.Utils.getOrDefault;
import static java.util.Objects.isNull; import static java.util.Objects.isNull;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import dev.langchain4j.Internal;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import dev.langchain4j.Internal; import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import software.amazon.awssdk.core.interceptor.Context; 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); 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 logRequests;
private final boolean logResponses; private final boolean logResponses;
private final Logger logger; private final Logger logger;
@ -51,7 +60,8 @@ class AwsLoggingInterceptor implements ExecutionInterceptor {
if (logRequests) { if (logRequests) {
if (request.method() == SdkHttpMethod.POST && request instanceof SdkHttpFullRequest sdkHttpFullRequest) { if (request.method() == SdkHttpMethod.POST && request instanceof SdkHttpFullRequest sdkHttpFullRequest) {
try { try {
ContentStreamProvider csp = sdkHttpFullRequest.contentStreamProvider().orElse(null); ContentStreamProvider csp =
sdkHttpFullRequest.contentStreamProvider().orElse(null);
if (nonNull(csp)) body = IoUtils.toUtf8String(csp.newStream()); if (nonNull(csp)) body = IoUtils.toUtf8String(csp.newStream());
} catch (IOException e) { } catch (IOException e) {
logger.warn("Unable to obtain request body", e); logger.warn("Unable to obtain request body", e);
@ -61,10 +71,9 @@ class AwsLoggingInterceptor implements ExecutionInterceptor {
"Request:\n- method: {}\n- url: {}\n- headers: {}\n- query parameters: {}\n- body: {}", "Request:\n- method: {}\n- url: {}\n- headers: {}\n- query parameters: {}\n- body: {}",
request.method(), request.method(),
request.getUri(), request.getUri(),
request.headers(), maskHeaders(request.headers()),
request.rawQueryParameters(), request.rawQueryParameters(),
body body);
);
} }
} }
@ -80,7 +89,7 @@ class AwsLoggingInterceptor implements ExecutionInterceptor {
logger.debug( logger.debug(
"Response Status: {} \nHeaders: {} \nResponse Body Type: {}", "Response Status: {} \nHeaders: {} \nResponse Body Type: {}",
response.statusCode(), response.statusCode(),
response.headers(), maskHeaders(response.headers()),
context.response().getClass().getSimpleName()); context.response().getClass().getSimpleName());
} }
} }
@ -100,4 +109,24 @@ class AwsLoggingInterceptor implements ExecutionInterceptor {
} }
return isNull(content) ? Optional.empty() : Optional.of(new ByteArrayInputStream(content)); 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(", ", "{", "}"));
}
} }

View File

@ -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("{}");
}
}