Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .secrets.baseline

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 41 additions & 3 deletions src/main/java/com/ibm/cloud/sdk/core/util/LoggingUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
package com.ibm.cloud.sdk.core.util;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -43,11 +46,47 @@ private LoggingUtils() { }
private static final String redactedTokens = String.join("|", Arrays.asList(redactedKeywords));
private static final String redacted = "[redacted]";

private static final Pattern reAuthHeader = Pattern.compile("(?m)^(Authorization|X-Auth\\S*): .*");
// Names (lower-case) of HTTP headers whose values must never be logged.
// In addition to these, any header whose name starts with "x-auth" is also redacted.
// NOTE: keep this list consistent with the other SDK cores (Go, Node, Python).
private static final Set<String> redactedHeaderNames = new HashSet<>(Arrays.asList(
"authorization",
"proxy-authorization",
"cookie",
"set-cookie",
"secret"));
private static final String redactedHeaderPrefix = "x-auth";

// Matches a single "Name: value" header line (line terminators are not consumed).
private static final Pattern reHeaderLine = Pattern.compile("(?m)^([!#$%&'*+.^_`|~0-9A-Za-z-]+):[^\\r\\n]*");
private static final Pattern rePropertySetting = Pattern.compile("(?im)(" + redactedTokens + ")=[^&\\n]*(&|$)");
private static final Pattern reJsonField =
Pattern.compile("(?i)\"([^\"]*(" + redactedTokens + ")[^\"_]*)\":\\s*\"[^\"\\n]*\"");

/**
* Returns true iff the header "name" (compared case-insensitively) is one whose value must be redacted.
*/
private static boolean isRedactedHeader(String name) {
String lowerName = name.toLowerCase(Locale.ROOT);
return redactedHeaderNames.contains(lowerName) || lowerName.startsWith(redactedHeaderPrefix);
}

/**
* Redacts the value of each credential-bearing HTTP header found in "s".
* Header names are parsed line-by-line and compared case-insensitively.
*/
private static String redactHeaders(String s) {
Matcher matcher = reHeaderLine.matcher(s);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String name = matcher.group(1);
String replacement = isRedactedHeader(name) ? name + ": " + redacted : matcher.group();
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(sb);
return sb.toString();
}

/**
* Redacts secrets within string "s" and returns the resulting string
* with secrets replaced by "[redacted]".
Expand All @@ -59,8 +98,7 @@ public static String redactSecrets(String s) {
String redactedString = s;

// Redact secrets within any special headers.
Matcher matcher1 = reAuthHeader.matcher(redactedString);
redactedString = matcher1.replaceAll("$1: " + redacted);
redactedString = redactHeaders(redactedString);

// Redact secrets within property settings.
Matcher matcher2 = rePropertySetting.matcher(redactedString);
Expand Down
51 changes: 51 additions & 0 deletions src/test/java/com/ibm/cloud/sdk/core/util/LoggingUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,57 @@ public void testRedactHeaders() {
assertEquals(LoggingUtils.redactSecrets(input), expected);
}

@Test
public void testRedactCredentialHeaders() {
String[] headers = {
"Authorization", "Proxy-Authorization", "Cookie", "Set-Cookie", "Secret",
"X-Auth-Token", "X-Authorization", "X-Auth-Foo-Bar" };
for (String header : headers) {
for (String name : new String[] { header, header.toLowerCase(), header.toUpperCase() }) {
String result = LoggingUtils.redactSecrets(name + ": s3cr3tvalue");
assertFalse(result.contains("s3cr3tvalue"), "header: " + name);
assertEquals(result, name + ": [redacted]");
}
}

// Cookie-style values with multiple attributes.
String result = LoggingUtils.redactSecrets("Set-Cookie: sid=abc123; Path=/; HttpOnly");
assertFalse(result.contains("abc123"));
result = LoggingUtils.redactSecrets("Cookie: a=firstval; b=secondval");
assertFalse(result.contains("firstval"));
assertFalse(result.contains("secondval"));

// A full CRLF-delimited HTTP dump: only credential headers are redacted, CRLF preserved.
String dump = "POST /v1/token HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "authorization: Bearer tok1\r\n"
+ "Proxy-Authorization: Basic tok2\r\n"
+ "COOKIE: sid=tok3\r\n"
+ "Content-Type: application/json\r\n"
+ "X-Auth-Token: tok4\r\n"
+ "\r\n"
+ "body\r\n";
result = LoggingUtils.redactSecrets(dump);
for (String tok : new String[] { "tok1", "tok2", "tok3", "tok4" }) {
assertFalse(result.contains(tok), tok);
}
assertTrue(result.contains("Host: example.com\r\n"));
assertTrue(result.contains("Content-Type: application/json\r\n"));
assertTrue(result.contains("authorization: [redacted]\r\n"));
assertTrue(result.contains("\r\n\r\nbody\r\n"));

// Response containing Set-Cookie.
result = LoggingUtils.redactSecrets("200 OK\nSet-Cookie: session=tok5\nContent-Length: 2\n\n{}");
assertFalse(result.contains("tok5"));
assertTrue(result.contains("Content-Length: 2"));

// Non-credential headers, and names that merely resemble credential names, are untouched.
for (String h : new String[] {
"Content-Type: text/plain", "Accept: */*", "X-Request-Id: abc", "Cookies: yes", "Xx-Auth: keep" }) {
assertEquals(LoggingUtils.redactSecrets(h), h);
}
}

@Test
public void testRedactProperties() {
assertEquals(LoggingUtils.redactSecrets("...&apikey=foo&..."), "...&apikey=[redacted]&...");
Expand Down
Loading