-
Notifications
You must be signed in to change notification settings - Fork 52
fix(schema): relax requestHeaderAllowlist validation to match runtime contract #1155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,20 @@ | ||
| import { | ||
| HEADER_ALLOWLIST_PREFIX as HEADER_ALLOWLIST_PREFIX_FROM_SCHEMA, | ||
| MAX_HEADER_ALLOWLIST_SIZE as MAX_HEADER_ALLOWLIST_SIZE_FROM_SCHEMA, | ||
| getHeaderRejectionReason, | ||
| } from '../../../schema/schemas/agent-env'; | ||
|
|
||
| export const HEADER_ALLOWLIST_PREFIX = HEADER_ALLOWLIST_PREFIX_FROM_SCHEMA; | ||
| export const MAX_HEADER_ALLOWLIST_SIZE = MAX_HEADER_ALLOWLIST_SIZE_FROM_SCHEMA; | ||
|
|
||
| const HEADER_NAME_PATTERN = /^[A-Za-z0-9-]+$/; | ||
|
|
||
| /** | ||
| * Normalize a header name according to AgentCore Runtime rules: | ||
| * - "Authorization" (case-insensitive) -> "Authorization" | ||
| * - Headers already starting with the prefix (case-insensitive) -> canonical prefix + original suffix | ||
| * - Other headers -> prepend the prefix | ||
| * - Headers already starting with the AgentCore custom prefix | ||
| * (case-insensitive) -> canonical prefix + original suffix | ||
| * - Otherwise -> the input is returned unchanged. The allowlist now accepts any | ||
| * non-restricted HTTP header name (alphanumerics, hyphens, underscores), so | ||
| * we no longer auto-prepend the AgentCore custom prefix. | ||
| */ | ||
| export function normalizeHeaderName(input: string): string { | ||
| if (input.toLowerCase() === 'authorization') { | ||
|
|
@@ -21,12 +23,12 @@ export function normalizeHeaderName(input: string): string { | |
| if (input.toLowerCase().startsWith(HEADER_ALLOWLIST_PREFIX.toLowerCase())) { | ||
| return `${HEADER_ALLOWLIST_PREFIX}${input.slice(HEADER_ALLOWLIST_PREFIX.length)}`; | ||
| } | ||
| return `${HEADER_ALLOWLIST_PREFIX}${input}`; | ||
| return input; | ||
| } | ||
|
|
||
| /** | ||
| * Parse a comma-separated string of header names, normalize each, and deduplicate. | ||
| * Returns an array of normalized header names. | ||
| * Parse a comma-separated string of header names, normalize each, and deduplicate | ||
| * (case-insensitive; first occurrence wins). | ||
| */ | ||
| export function parseAndNormalizeHeaders(input: string): string[] { | ||
| const headers = input | ||
|
|
@@ -35,7 +37,16 @@ export function parseAndNormalizeHeaders(input: string): string[] { | |
| .filter(Boolean) | ||
| .map(normalizeHeaderName); | ||
|
|
||
| return Array.from(new Set(headers)); | ||
| const seen = new Set<string>(); | ||
| const result: string[] = []; | ||
| for (const h of headers) { | ||
| const key = h.toLowerCase(); | ||
| if (!seen.has(key)) { | ||
| seen.add(key); | ||
| result.push(h); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -52,16 +63,30 @@ export function validateHeaderAllowlist(value: string): { success: boolean; erro | |
| .split(',') | ||
| .map(s => s.trim()) | ||
| .filter(Boolean); | ||
|
|
||
| // Validate each header name against the allowlist rules (regex + restricted | ||
| // names + reserved prefixes). | ||
| for (const name of rawNames) { | ||
| if (!HEADER_NAME_PATTERN.test(name)) { | ||
| const rejection = getHeaderRejectionReason(normalizeHeaderName(name)); | ||
| if (rejection) { | ||
| return { success: false, error: rejection }; | ||
| } | ||
| } | ||
|
|
||
| // Detect duplicates (case-insensitive, after normalization). | ||
| const headers = parseAndNormalizeHeaders(value); | ||
| const seen = new Set<string>(); | ||
| for (const raw of rawNames) { | ||
| const key = normalizeHeaderName(raw).toLowerCase(); | ||
| if (seen.has(key)) { | ||
| return { | ||
| success: false, | ||
| error: `Invalid header name "${name}". Header names may only contain letters, numbers, and hyphens.`, | ||
| error: `Duplicate header (case-insensitive): "${raw}".`, | ||
| }; | ||
| } | ||
| seen.add(key); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: |
||
| } | ||
|
|
||
| const headers = parseAndNormalizeHeaders(value); | ||
| if (headers.length > MAX_HEADER_ALLOWLIST_SIZE) { | ||
| return { | ||
| success: false, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Breaking behavior change for
agentcore invoke -Handagentcore dev -H.Because
parseHeaderFlag(→normalizeHeaderName) is on the hot path for the-H/--headerflags in bothsrc/cli/commands/invoke/command.tsxandsrc/cli/commands/dev/command.tsx, and the resulting headers are forwarded as literal HTTP headers to the runtime (seeinvoke/action.tslines 161/245/325/369/426/460), this change alters what gets sent on the wire. Previously:sent
X-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeader: val. After this PR it sendsMyHeader: valverbatim. Users with an existing deployed runtime whose allowlist still containsX-Amzn-Bedrock-AgentCore-Runtime-Custom-MyHeaderwill silently stop receiving the header at the agent — the invoke will appear to succeed but the agent won't see the value. This is a behavior break that is distinct from the schema relaxation and isn't called out in the PR description orCHANGELOG.md.Options to address:
parseHeaderFlag(the invoke/dev CLI path) and remove it only from the allowlist-normalization path. The two paths don't actually need to share normalization — one configures the runtime, the other talks to it.CHANGELOG.md, and ideally haveparseHeaderFlagemit a one-time warning when a bare name (notAuthorization, notX-Amzn-Bedrock-…-Custom-) is passed via-H, suggesting the user either add the full prefix or the literal name to their allowlist.