Skip to content

Commit 3944c8c

Browse files
fix(security): never expose credentials via the request observer or test report
Security review findings on the reporting feature, all addressed: - AuthInterceptor: the RequestObserver now receives a defensive request copy with authtoken/authorization/access_token headers masked - the production seam can no longer hand out credentials even if abused - TestReporter: request cURL bodies and response bodies are passed through redactSecrets() before entering the report - passwords, session authtokens, management/delivery token values (cs*), OAuth and TOTP secrets are replaced with ***REDACTED***. The report ships as a CI artifact, so no live secret may survive into it (previously the login cURL contained the account password and token-creation responses contained live token values) Verified: suite 252/0 failures; regenerated report scan shows zero authtoken values, zero cs* tokens, zero password strings (23 redaction markers). The one session token exposed in a local report was invalidated via logout.
1 parent ae6a735 commit 3944c8c

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

src/main/java/com/contentstack/cms/core/AuthInterceptor.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,30 @@ public Response intercept(Chain chain) throws IOException {
103103
RequestObserver localObserver = observer;
104104
if (localObserver != null) {
105105
try {
106-
localObserver.onCall(finalRequest, response, System.currentTimeMillis() - startedAt);
106+
// Never hand credentials to observers: sensitive headers are
107+
// masked on a defensive copy before the callback sees it.
108+
localObserver.onCall(maskSensitiveHeaders(finalRequest), response,
109+
System.currentTimeMillis() - startedAt);
107110
} catch (Exception ignored) {
108111
// observers must never break real requests
109112
}
110113
}
111114
return response;
112115
}
113116

117+
private static Request maskSensitiveHeaders(Request request) {
118+
Request.Builder masked = request.newBuilder();
119+
for (String name : new String[]{"authtoken", "authorization", "access_token"}) {
120+
String value = request.header(name);
121+
if (value != null) {
122+
masked.header(name, value.length() > 12
123+
? value.substring(0, 6) + "..." + value.substring(value.length() - 4)
124+
: "***");
125+
}
126+
}
127+
return masked.build();
128+
}
129+
114130
/**
115131
* Observer for outgoing requests/responses. Intended for test harnesses
116132
* and diagnostics (e.g. capturing cURL commands for test reports);

src/test/java/com/contentstack/cms/TestReporter.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ private static void captureCall(Request request, Response response, long duratio
107107
call.sdkMethod = detectSdkMethod(request.method(), request.url().encodedPath());
108108
try {
109109
String body = response.peekBody(MAX_BODY_CHARS + 1).string();
110+
body = redactSecrets(body);
110111
call.responseBody = body.length() > MAX_BODY_CHARS
111112
? body.substring(0, MAX_BODY_CHARS) + "\n... (truncated)" : body;
112113
} catch (Exception e) {
@@ -132,7 +133,7 @@ private static String toCurl(Request request) {
132133
if (len > 0 && len < 65536) {
133134
Buffer buffer = new Buffer();
134135
request.body().writeTo(buffer);
135-
String body = buffer.readString(StandardCharsets.UTF_8).replace("'", "'\\''");
136+
String body = redactSecrets(buffer.readString(StandardCharsets.UTF_8)).replace("'", "'\\''");
136137
curl.append(" \\\n -d '").append(body).append("'");
137138
}
138139
} catch (Exception ignored) {
@@ -142,6 +143,27 @@ private static String toCurl(Request request) {
142143
return curl.toString();
143144
}
144145

146+
/**
147+
* Redacts credential values from JSON payloads before they enter the report:
148+
* passwords, auth/session tokens, management/delivery token values, OAuth
149+
* secrets and TOTP secrets. The report is shipped as a CI artifact, so no
150+
* live secret may survive into it.
151+
*/
152+
private static final Pattern SECRET_JSON_FIELDS = Pattern.compile(
153+
"(\"(?:password|authtoken|token|management_token|secret|mfaSecret|tfaToken|tfa_token|"
154+
+ "access_token|refresh_token|client_secret|api_secret)\"\\s*:\\s*\")([^\"]*)(\")",
155+
Pattern.CASE_INSENSITIVE);
156+
private static final Pattern CS_TOKEN_PATTERN = Pattern.compile("\\bcs[a-f0-9]{16,}\\b");
157+
158+
static String redactSecrets(String text) {
159+
if (text == null || text.isEmpty()) {
160+
return text;
161+
}
162+
String redacted = SECRET_JSON_FIELDS.matcher(text).replaceAll("$1***REDACTED***$3");
163+
redacted = CS_TOKEN_PATTERN.matcher(redacted).replaceAll("cs***REDACTED***");
164+
return redacted;
165+
}
166+
145167
private static String mask(String headerName, String value) {
146168
String lower = headerName.toLowerCase();
147169
if ((lower.equals("authtoken") || lower.equals("authorization") || lower.equals("access_token"))

0 commit comments

Comments
 (0)