Skip to content

Commit a232118

Browse files
committed
fix: relax custom header regex to allow any valid HTTP header (#1151)
The AgentCore Runtime service now accepts any valid HTTP header in requestHeaderAllowlist, not just those prefixed with X-Amzn-Bedrock-AgentCore-Runtime-Custom-. Remove auto-prefixing and replace the strict allowlist with service-aligned validation rules: block x-amz-/x-amzn- prefixes and restricted standard headers.
1 parent eb2e147 commit a232118

6 files changed

Lines changed: 224 additions & 91 deletions

File tree

src/cli/commands/shared/__tests__/header-utils.test.ts

Lines changed: 62 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,20 @@ import {
88
import { describe, expect, it } from 'vitest';
99

1010
describe('normalizeHeaderName', () => {
11-
it('returns "Authorization" as-is', () => {
11+
it('returns "Authorization" for case-insensitive match', () => {
1212
expect(normalizeHeaderName('Authorization')).toBe('Authorization');
13-
});
14-
15-
it('normalizes case-insensitive "authorization" to "Authorization"', () => {
1613
expect(normalizeHeaderName('authorization')).toBe('Authorization');
1714
expect(normalizeHeaderName('AUTHORIZATION')).toBe('Authorization');
1815
expect(normalizeHeaderName('AuThOrIzAtIoN')).toBe('Authorization');
1916
});
2017

21-
it('returns full header name with canonical prefix when prefix already present', () => {
22-
const fullHeader = 'X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader';
23-
expect(normalizeHeaderName(fullHeader)).toBe(fullHeader);
24-
});
25-
26-
it('normalizes prefix casing to canonical form', () => {
27-
expect(normalizeHeaderName('x-amzn-bedrock-agentcore-runtime-custom-MyHeader')).toBe(
28-
'X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader'
18+
it('returns other headers as-is without prefixing', () => {
19+
expect(normalizeHeaderName('MyHeader')).toBe('MyHeader');
20+
expect(normalizeHeaderName('X-Custom-Signature')).toBe('X-Custom-Signature');
21+
expect(normalizeHeaderName('X-Api-Key')).toBe('X-Api-Key');
22+
expect(normalizeHeaderName('X-Amzn-Bedrock-AgentCore-Runtime-Custom-Foo')).toBe(
23+
'X-Amzn-Bedrock-AgentCore-Runtime-Custom-Foo'
2924
);
30-
expect(normalizeHeaderName('X-AMZN-BEDROCK-AGENTCORE-RUNTIME-CUSTOM-MyHeader')).toBe(
31-
'X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader'
32-
);
33-
});
34-
35-
it('auto-prefixes a bare suffix like "MyHeader"', () => {
36-
expect(normalizeHeaderName('MyHeader')).toBe('X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader');
37-
});
38-
39-
it('auto-prefixes suffix with hyphens like "My-Custom-Header"', () => {
40-
expect(normalizeHeaderName('My-Custom-Header')).toBe('X-Amzn-Bedrock-AgentCore-Runtime-Custom-My-Custom-Header');
4125
});
4226
});
4327

@@ -50,18 +34,14 @@ describe('parseAndNormalizeHeaders', () => {
5034
expect(parseAndNormalizeHeaders(' , , ')).toEqual([]);
5135
});
5236

53-
it('splits comma-separated and normalizes', () => {
54-
const result = parseAndNormalizeHeaders('MyHeader, authorization, Another-Header');
55-
expect(result).toEqual([
56-
'X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader',
57-
'Authorization',
58-
'X-Amzn-Bedrock-AgentCore-Runtime-Custom-Another-Header',
59-
]);
37+
it('splits comma-separated headers', () => {
38+
const result = parseAndNormalizeHeaders('X-Custom-Signature, authorization, X-Api-Key');
39+
expect(result).toEqual(['X-Custom-Signature', 'Authorization', 'X-Api-Key']);
6040
});
6141

62-
it('deduplicates after normalization', () => {
63-
const result = parseAndNormalizeHeaders('MyHeader, X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader');
64-
expect(result).toEqual(['X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader']);
42+
it('deduplicates case-insensitively keeping first occurrence', () => {
43+
const result = parseAndNormalizeHeaders('My-Header, MY-HEADER, my-header');
44+
expect(result).toEqual(['My-Header']);
6545
});
6646

6747
it('deduplicates case-insensitive Authorization', () => {
@@ -70,12 +50,8 @@ describe('parseAndNormalizeHeaders', () => {
7050
});
7151

7252
it('trims whitespace around values', () => {
73-
const result = parseAndNormalizeHeaders(' MyHeader , authorization , Another-Header ');
74-
expect(result).toEqual([
75-
'X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader',
76-
'Authorization',
77-
'X-Amzn-Bedrock-AgentCore-Runtime-Custom-Another-Header',
78-
]);
53+
const result = parseAndNormalizeHeaders(' X-Custom , X-Api-Key ');
54+
expect(result).toEqual(['X-Custom', 'X-Api-Key']);
7955
});
8056
});
8157

@@ -85,34 +61,52 @@ describe('validateHeaderAllowlist', () => {
8561
expect(validateHeaderAllowlist(' ')).toEqual({ success: true });
8662
});
8763

88-
it('returns success for valid custom header suffix', () => {
89-
expect(validateHeaderAllowlist('MyHeader')).toEqual({ success: true });
90-
});
91-
92-
it('returns success for valid full header name', () => {
93-
expect(validateHeaderAllowlist('X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader')).toEqual({ success: true });
64+
it('returns success for valid custom headers', () => {
65+
expect(validateHeaderAllowlist('X-Custom-Signature')).toEqual({ success: true });
66+
expect(validateHeaderAllowlist('X-Api-Key')).toEqual({ success: true });
67+
expect(validateHeaderAllowlist('My_Header')).toEqual({ success: true });
9468
});
9569

96-
it('returns success for "Authorization"', () => {
70+
it('returns success for Authorization', () => {
9771
expect(validateHeaderAllowlist('Authorization')).toEqual({ success: true });
9872
expect(validateHeaderAllowlist('authorization')).toEqual({ success: true });
9973
});
10074

75+
it('returns success for the legacy prefix', () => {
76+
expect(validateHeaderAllowlist('X-Amzn-Bedrock-AgentCore-Runtime-Custom-Foo')).toEqual({ success: true });
77+
});
78+
10179
it('returns success for mixed valid headers', () => {
102-
expect(validateHeaderAllowlist('Authorization, MyHeader, X-Amzn-Bedrock-AgentCore-Runtime-Custom-Another')).toEqual(
103-
{ success: true }
104-
);
80+
expect(validateHeaderAllowlist('Authorization, X-Custom-Signature, X-Api-Key')).toEqual({ success: true });
81+
});
82+
83+
it('returns error for x-amz- prefix', () => {
84+
const result = validateHeaderAllowlist('x-amz-security-token');
85+
expect(result.success).toBe(false);
86+
expect(result.error).toContain('x-amz-');
87+
});
88+
89+
it('returns error for x-amzn- prefix without allowed exception', () => {
90+
const result = validateHeaderAllowlist('x-amzn-something');
91+
expect(result.success).toBe(false);
92+
expect(result.error).toContain('x-amzn-');
93+
});
94+
95+
it('returns error for restricted headers', () => {
96+
const result = validateHeaderAllowlist('Content-Type');
97+
expect(result.success).toBe(false);
98+
expect(result.error).toContain('restricted');
10599
});
106100

107101
it('returns error when exceeding max 20 headers', () => {
108-
const headers = Array.from({ length: 21 }, (_, i) => `Header${i}`).join(', ');
102+
const headers = Array.from({ length: 21 }, (_, i) => `X-Header-${i}`).join(', ');
109103
const result = validateHeaderAllowlist(headers);
110104
expect(result.success).toBe(false);
111105
expect(result.error).toContain('20');
112106
});
113107

114108
it('returns success for exactly 20 headers', () => {
115-
const headers = Array.from({ length: 20 }, (_, i) => `Header${i}`).join(', ');
109+
const headers = Array.from({ length: 20 }, (_, i) => `X-Header-${i}`).join(', ');
116110
expect(validateHeaderAllowlist(headers)).toEqual({ success: true });
117111
});
118112

@@ -127,20 +121,24 @@ describe('validateHeaderAllowlist', () => {
127121
expect(result.success).toBe(false);
128122
expect(result.error).toContain('Invalid header name');
129123
});
124+
125+
it('allows underscores in header names', () => {
126+
expect(validateHeaderAllowlist('My_Custom_Header')).toEqual({ success: true });
127+
});
130128
});
131129

132130
describe('parseHeaderFlag', () => {
133131
it('parses "Key: Value" format', () => {
134-
expect(parseHeaderFlag('MyHeader: some-value')).toEqual({
135-
name: 'X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader',
136-
value: 'some-value',
132+
expect(parseHeaderFlag('X-Custom-Signature: sha256=abc123')).toEqual({
133+
name: 'X-Custom-Signature',
134+
value: 'sha256=abc123',
137135
});
138136
});
139137

140138
it('parses "Key:Value" format without space', () => {
141-
expect(parseHeaderFlag('MyHeader:some-value')).toEqual({
142-
name: 'X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader',
143-
value: 'some-value',
139+
expect(parseHeaderFlag('X-Api-Key:my-key')).toEqual({
140+
name: 'X-Api-Key',
141+
value: 'my-key',
144142
});
145143
});
146144

@@ -151,7 +149,7 @@ describe('parseHeaderFlag', () => {
151149
});
152150
});
153151

154-
it('normalizes header names', () => {
152+
it('normalizes Authorization casing', () => {
155153
expect(parseHeaderFlag('authorization: token')).toEqual({
156154
name: 'Authorization',
157155
value: 'token',
@@ -167,18 +165,18 @@ describe('parseHeaderFlag', () => {
167165
});
168166

169167
it('trims whitespace from key and value', () => {
170-
expect(parseHeaderFlag(' MyHeader : some-value ')).toEqual({
171-
name: 'X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader',
168+
expect(parseHeaderFlag(' X-Custom : some-value ')).toEqual({
169+
name: 'X-Custom',
172170
value: 'some-value',
173171
});
174172
});
175173
});
176174

177175
describe('parseHeaderFlags', () => {
178176
it('parses multiple headers', () => {
179-
const result = parseHeaderFlags(['MyHeader: value1', 'Authorization: Bearer token']);
177+
const result = parseHeaderFlags(['X-Custom-Signature: value1', 'Authorization: Bearer token']);
180178
expect(result).toEqual({
181-
'X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader': 'value1',
179+
'X-Custom-Signature': 'value1',
182180
Authorization: 'Bearer token',
183181
});
184182
});
@@ -188,9 +186,9 @@ describe('parseHeaderFlags', () => {
188186
});
189187

190188
it('last value wins for duplicate keys', () => {
191-
const result = parseHeaderFlags(['MyHeader: first', 'MyHeader: second']);
189+
const result = parseHeaderFlags(['X-Custom: first', 'X-Custom: second']);
192190
expect(result).toEqual({
193-
'X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader': 'second',
191+
'X-Custom': 'second',
194192
});
195193
});
196194

0 commit comments

Comments
 (0)