-
Notifications
You must be signed in to change notification settings - Fork 1.6k
fix(security): redact invalid Azure authentication credentials #2421
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 4 commits
5e2b22a
222e643
7ff52b6
eba9440
953b385
c006e3b
e4947bd
01a5284
63f427f
8ea03dc
1cecd41
7f1728c
2a85796
62f306e
c300207
59af3c0
c04e3d0
fd1ebc1
58e88e8
36cc2bc
5704232
1695ca7
99d8e77
c5299da
476508d
2111205
94bfb1c
c611ead
cf44e55
93c4ee3
860d06b
b9d4baa
9c7a930
828d891
fc4957f
8c9378a
33c43b7
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,6 +1,6 @@ | ||
| import type { RequestInit, RequestInfo, Response } from './internal/builtin-types'; | ||
| import type { NullableHeaders } from './internal/headers'; | ||
| import { buildHeaders } from './internal/headers'; | ||
| import { buildAzureAuthenticationHeaders, buildHeaders } from './internal/headers'; | ||
| import * as Errors from './error'; | ||
| import type { FinalRequestOptions } from './internal/request-options'; | ||
| import { isObj, readEnv } from './internal/utils'; | ||
|
|
@@ -126,6 +126,7 @@ export class AzureOpenAI extends OpenAI { | |
| throw new Errors.OpenAIError('baseURL and endpoint are mutually exclusive'); | ||
| } | ||
|
|
||
| protectAzureAmbientHeaders(opts); | ||
| super({ | ||
| apiKey: azureADTokenProvider ?? apiKey, | ||
| baseURL, | ||
|
|
@@ -183,7 +184,9 @@ export class AzureOpenAI extends OpenAI { | |
| controller: AbortController, | ||
| schemes?: { bearerAuth?: boolean; adminAPIKeyAuth?: boolean }, | ||
| ): Promise<Response> { | ||
| if (new Headers(init.headers).has('api-key')) { | ||
| const headers = buildHeaders([buildAzureAuthenticationHeaders(), init.headers]).values; | ||
| init.headers = headers; | ||
| if (headers.has('api-key')) { | ||
|
Comment on lines
+282
to
+283
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.
When an Azure AGENTS.md reference: AGENTS.md:L52-L57 Useful? React with 👍 / 👎.
Contributor
Author
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. Valid at |
||
| init.redirect = 'manual'; | ||
| } | ||
|
|
||
|
|
@@ -196,9 +199,33 @@ export class AzureOpenAI extends OpenAI { | |
| ): Promise<NullableHeaders | undefined> { | ||
| const security = schemes ?? { bearerAuth: true, adminAPIKeyAuth: true }; | ||
| if (security.bearerAuth && typeof this._options.apiKey === 'string') { | ||
| return buildHeaders([{ 'api-key': this.apiKey }]); | ||
| return buildAzureAuthenticationHeaders([['api-key', this.apiKey]]); | ||
| } | ||
| return super.authHeaders(opts, security); | ||
|
|
||
| return buildAzureAuthenticationHeaders( | ||
| security.bearerAuth ? await this.bearerAuth(opts) : undefined, | ||
| security.adminAPIKeyAuth ? await this.adminAPIKeyAuth(opts) : undefined, | ||
| ); | ||
| } | ||
|
|
||
| protected override async bearerAuth(_opts: FinalRequestOptions): Promise<NullableHeaders | undefined> { | ||
| if (this.apiKey === null) { | ||
| return undefined; | ||
| } | ||
| return buildAzureAuthenticationHeaders([['Authorization', `Bearer ${this.apiKey}`]]); | ||
| } | ||
|
|
||
| protected override async adminAPIKeyAuth(_opts: FinalRequestOptions): Promise<NullableHeaders | undefined> { | ||
| if (this.adminAPIKey === null || this.adminAPIKey === undefined) { | ||
| return undefined; | ||
| } | ||
| return buildAzureAuthenticationHeaders([['Authorization', `Bearer ${this.adminAPIKey}`]]); | ||
| } | ||
| } | ||
|
|
||
| function protectAzureAmbientHeaders(options: Pick<ClientOptions, 'defaultHeaders'>): void { | ||
| if (readEnv('OPENAI_CUSTOM_HEADERS')) { | ||
| options.defaultHeaders = buildAzureAuthenticationHeaders(options.defaultHeaders); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /** Rejects invalid HTTP-field bytes without exposing a private Azure credential. */ | ||
| export function assertAzureCredentialHeaderValue(value: string): void { | ||
| for (const character of value) { | ||
| const code = character.codePointAt(0) ?? 0; | ||
| if ((code < 0x20 && code !== 0x09) || code === 0x7f || code > 0xff) { | ||
| throw new TypeError('Azure OpenAI credential contains an invalid HTTP header value.'); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** Identifies the two credential-bearing Azure HTTP header fields. */ | ||
| export function isAzureAuthenticationHeader(name: string): boolean { | ||
| const normalized = name.toLowerCase(); | ||
| return normalized === 'authorization' || normalized === 'api-key'; | ||
| } | ||
|
|
||
| /** | ||
| * Collapses case-insensitive WebSocket credential overrides before validating | ||
| * only the effective values. Callers supply SDK-created plain header records. | ||
| */ | ||
| export function safeAzureWebSocketHeaders<Headers extends Record<string, unknown>>( | ||
| headers: Headers, | ||
| ): Headers { | ||
| const safeHeaders = new Map<string, unknown>(); | ||
| const authenticationNames = new Map<string, string>(); | ||
|
|
||
| for (const [name, value] of Object.entries(headers)) { | ||
| if (!isAzureAuthenticationHeader(name)) { | ||
| safeHeaders.set(name, value); | ||
| continue; | ||
| } | ||
|
|
||
| const normalized = name.toLowerCase(); | ||
| const previousName = authenticationNames.get(normalized); | ||
| if (previousName !== undefined) { | ||
| safeHeaders.delete(previousName); | ||
| authenticationNames.delete(normalized); | ||
| } | ||
| if (value === null || value === undefined) { | ||
| continue; | ||
| } | ||
| safeHeaders.set(name, value); | ||
| authenticationNames.set(normalized, name); | ||
| } | ||
|
|
||
| for (const name of authenticationNames.values()) { | ||
| const value = safeHeaders.get(name); | ||
| const values = Array.isArray(value) ? value : [value]; | ||
| for (const entry of values) { | ||
|
HAYDEN-OAI marked this conversation as resolved.
Outdated
|
||
| if (typeof entry === 'string') { | ||
| assertAzureCredentialHeaderValue(entry); | ||
| } | ||
| } | ||
| } | ||
| return Object.fromEntries(safeHeaders) as Headers; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.