Skip to content

Commit a9b0160

Browse files
authored
fix(invoke): adjust redaction regex to allow words following bearer (#1480)
1 parent 96be003 commit a9b0160

4 files changed

Lines changed: 55 additions & 19 deletions

File tree

npm-shrinkwrap.json

Lines changed: 6 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
"eslint-plugin-security": "^4.0.0",
148148
"husky": "^9.1.7",
149149
"ink-testing-library": "^4.0.0",
150+
"jose": "^6.2.3",
150151
"lint-staged": "^16.2.7",
151152
"node-pty": "^1.1.0",
152153
"prettier": "^3.7.4",

src/cli/commands/invoke/__tests__/redact-sensitive-text.test.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
11
import { redactSensitiveText } from '../command.js';
2-
import { describe, expect, it } from 'vitest';
2+
import { SignJWT } from 'jose';
3+
import { beforeAll, describe, expect, it } from 'vitest';
4+
5+
const TEST_SIGNING_SECRET = new TextEncoder().encode('redaction-unit-test-signing-secret-0123456789');
6+
7+
async function makeJwt(claims: Record<string, unknown> = { sub: '1234567890', aud: 'client-abc' }): Promise<string> {
8+
return new SignJWT(claims)
9+
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
10+
.setIssuedAt()
11+
.setExpirationTime('1h')
12+
.sign(TEST_SIGNING_SECRET);
13+
}
314

415
describe('redactSensitiveText', () => {
16+
let jwt: string;
17+
18+
beforeAll(async () => {
19+
jwt = await makeJwt();
20+
});
21+
522
it('redacts Bearer tokens', () => {
6-
expect(redactSensitiveText('Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.payload.sig')).toBe(
7-
'Authorization: Bearer [REDACTED]'
8-
);
23+
expect(redactSensitiveText(`Authorization: Bearer ${jwt}`)).toBe('Authorization: Bearer [REDACTED]');
924
});
1025

1126
it('redacts Bearer tokens in JSON', () => {
12-
expect(redactSensitiveText('{"header":"Bearer eyJhbGciOiJSUzI1NiJ9.payload.sig"}')).toBe(
13-
'{"header":"Bearer [REDACTED]"}'
14-
);
27+
expect(redactSensitiveText(`{"header":"Bearer ${jwt}"}`)).toBe('{"header":"Bearer [REDACTED]"}');
28+
});
29+
30+
it('redacts a JWT by shape even without a "bearer"/key prefix', () => {
31+
expect(redactSensitiveText(`agent response: ${jwt}`)).toBe('agent response: [REDACTED]');
1532
});
1633

1734
it('redacts client_secret in key=value form', () => {
@@ -38,13 +55,24 @@ describe('redactSensitiveText', () => {
3855
expect(redactSensitiveText('client-secret=mysecret')).toBe('client-secret=[REDACTED]');
3956
});
4057

58+
it('handles multiple sensitive values in one string', () => {
59+
expect(redactSensitiveText(`Bearer ${jwt} and client_secret=xyz789`)).toBe(
60+
'Bearer [REDACTED] and client_secret=[REDACTED]'
61+
);
62+
});
63+
4164
it('does not modify text without sensitive content', () => {
4265
const input = 'Agent responded successfully with 200 OK';
4366
expect(redactSensitiveText(input)).toBe(input);
4467
});
4568

46-
it('handles multiple sensitive values in one string', () => {
47-
const input = 'Bearer abc123 and client_secret=xyz789';
48-
expect(redactSensitiveText(input)).toBe('Bearer [REDACTED] and client_secret=[REDACTED]');
69+
it('does not redact the literal word "token" after "bearer"', () => {
70+
const input = "Agent 'E2eJwt123' is configured for CUSTOM_JWT but no bearer token is available.";
71+
expect(redactSensitiveText(input)).toBe(input);
72+
});
73+
74+
it('does not redact prose like "Invalid Bearer Token"', () => {
75+
const input = 'Invalid Bearer Token';
76+
expect(redactSensitiveText(input)).toBe(input);
4977
});
5078
});

src/cli/commands/invoke/command.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,16 @@ async function handleInvokeCLI(options: InvokeOptions, preloadedContext?: Invoke
8282
}
8383

8484
export function redactSensitiveText(value: string): string {
85-
return value
86-
.replace(/(bearer\s+)[a-z0-9\-._~+/]+=*/gi, '$1[REDACTED]')
87-
.replace(/(client[_-]?secret["']?\s*[:=]\s*["']?)([^"',\s}]+)/gi, '$1[REDACTED]')
88-
.replace(/((?:access[_-]?)?token["']?\s*[:=]\s*["']?)([^"',\s}]+)/gi, '$1[REDACTED]');
85+
return (
86+
value
87+
// AgentCore inbound bearer tokens are always CUSTOM_JWT/OIDC JWTs, and a JWT always begins
88+
// with `eyJ` (base64url of `{"`, the start of its header/payload JSON). Matching by shape
89+
// redacts the token wherever it appears and never touches prose like "bearer token".
90+
// See https://stackoverflow.com/a/74181595 (why JWTs start with `eyJ`).
91+
.replace(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, '[REDACTED]')
92+
.replace(/(client[_-]?secret["']?\s*[:=]\s*["']?)([^"',\s}]+)/gi, '$1[REDACTED]')
93+
.replace(/((?:access[_-]?)?token["']?\s*[:=]\s*["']?)([^"',\s}]+)/gi, '$1[REDACTED]')
94+
);
8995
}
9096

9197
function printInvokeResult(result: InvokeResult, options: InvokeOptions): void {

0 commit comments

Comments
 (0)