Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5e2b22a
fix(security): redact invalid Azure authentication credentials
HAYDEN-OAI Aug 19, 2026
222e643
fix(azure): protect effective HTTP and realtime credentials
HAYDEN-OAI Aug 19, 2026
7ff52b6
fix(azure): protect credentials across headers and hooks
HAYDEN-OAI Aug 19, 2026
eba9440
fix(azure): preserve deferred credential and transport hook contracts
HAYDEN-OAI Aug 19, 2026
953b385
fix(azure): preserve deferred credential header mutations
HAYDEN-OAI Aug 19, 2026
c006e3b
fix: preserve deferred Azure authentication header reads
HAYDEN-OAI Aug 19, 2026
e4947bd
fix(azure): expose deferred authentication tombstones
HAYDEN-OAI Aug 19, 2026
01a5284
fix(azure): bridge cross-runtime header iterator types
HAYDEN-OAI Aug 19, 2026
63f427f
fix(types): preserve Set iterator support on TypeScript 4.9
HAYDEN-OAI Aug 20, 2026
8ea03dc
fix(azure): snapshot socket arrays and normalize deferred headers
HAYDEN-OAI Aug 20, 2026
1cecd41
fix(azure): harden deferred header coercion and carrier compatibility
HAYDEN-OAI Aug 20, 2026
7f1728c
fix(azure): preserve trusted post-hook Headers identity
HAYDEN-OAI Aug 20, 2026
2a85796
fix(azure): protect preprocessing and deferred header boundaries
HAYDEN-OAI Aug 20, 2026
62f306e
fix(azure): snapshot websocket credential serialization
HAYDEN-OAI Aug 20, 2026
c300207
fix(azure): isolate request options and foreign headers
HAYDEN-OAI Aug 20, 2026
59af3c0
fix(azure): preserve authenticated request options identity
HAYDEN-OAI Aug 20, 2026
c04e3d0
fix(azure): isolate credential snapshots across concurrent requests
HAYDEN-OAI Aug 25, 2026
fd1ebc1
test(log): construct prototype-key fixtures safely
HAYDEN-OAI Aug 25, 2026
58e88e8
Merge branch 'main' into codex/azure-credential-header-privacy-202608…
HAYDEN-OAI Aug 25, 2026
36cc2bc
fix(azure): safely snapshot cross-realm Headers subclasses
HAYDEN-OAI Aug 25, 2026
5704232
fix(azure): satisfy credential regression lint rules
HAYDEN-OAI Aug 25, 2026
1695ca7
fix: isolate Azure credential header snapshots and markers
HAYDEN-OAI Aug 25, 2026
99d8e77
fix: satisfy Azure header snapshot lint rules
HAYDEN-OAI Aug 25, 2026
c5299da
fix: protect Azure credentials for stateful request bodies
HAYDEN-OAI Aug 25, 2026
476508d
fix(azure): preserve isolated deferred credential compatibility
HAYDEN-OAI Aug 25, 2026
2111205
fix(azure): sanitize hostile credential access and request headers
HAYDEN-OAI Aug 25, 2026
94bfb1c
fix(azure): bound header snapshots and sanitize proxy traps
HAYDEN-OAI Aug 25, 2026
c611ead
fix(azure): preserve body accessors and live header iteration
HAYDEN-OAI Aug 25, 2026
cf44e55
fix(azure): safely snapshot immutable credential accessors
HAYDEN-OAI Aug 25, 2026
93c4ee3
fix(azure): preserve accessor and cookie header semantics
HAYDEN-OAI Aug 25, 2026
860d06b
Merge origin/main into codex/azure-credential-header-privacy-20260819…
HAYDEN-OAI Aug 27, 2026
b9d4baa
fix(azure): bind credential snapshots to each request
HAYDEN-OAI Aug 27, 2026
9c7a930
fix(azure): isolate request-local credential capabilities
HAYDEN-OAI Aug 27, 2026
828d891
fix(azure): bind credential snapshots to request occurrences
HAYDEN-OAI Aug 27, 2026
fc4957f
fix(azure): iterate effective credential values directly
HAYDEN-OAI Aug 27, 2026
8c9378a
fix(azure): inspect credential tuple names without invoking getters
HAYDEN-OAI Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
753 changes: 738 additions & 15 deletions src/azure.ts

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions src/beta/realtime/websocket.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AzureOpenAI } from '../../index';
import { safeAzureCredentialHeaderValue } from '../../internal/azure';
import { assertBedrockWebSocketOrigin } from '../../internal/bedrock';
import { OpenAI } from '../../index';
import { OpenAIError } from '../../error';
Expand Down Expand Up @@ -119,11 +120,12 @@ function createAzureWebSocket(
throw new Error('Azure OpenAI Realtime requires an API key');
}

const credential = safeAzureCredentialHeaderValue(apiKey);
redactAzureCredentials(url, isBearerToken);
const socketURL = new URL(url);
socketURL.searchParams.delete('api-key');
socketURL.searchParams.delete('Authorization');
const headers = isBearerToken ? { Authorization: `Bearer ${apiKey}` } : { 'api-key': apiKey };
const headers = isBearerToken ? { Authorization: `Bearer ${credential}` } : { 'api-key': credential };

// @ts-ignore
return new WebSocket(socketURL.toString(), { protocols, headers });
Expand Down Expand Up @@ -186,10 +188,11 @@ export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter {
) {
super();
const hasProvider = typeof (client as any)?._options?.apiKey === 'function';
const apiKey = client?.apiKey;
const dangerouslyAllowBrowser =
props.dangerouslyAllowBrowser ??
(client as any)?._options?.dangerouslyAllowBrowser ??
(client?.apiKey?.startsWith('ek_') ? true : null);
(typeof apiKey === 'string' && apiKey.startsWith('ek_') ? true : null);
if (!dangerouslyAllowBrowser && isRunningInBrowserOrBrowserWorker()) {
throw new OpenAIError(
"It looks like you're running in a browser-like environment.\n\nThis is disabled by default, as it risks exposing your secret API credentials to attackers.\n\nYou can avoid this error by creating an ephemeral session token:\nhttps://platform.openai.com/docs/api-reference/realtime-sessions\n",
Expand Down
10 changes: 8 additions & 2 deletions src/beta/realtime/ws.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as WS from 'ws';
import { safeAzureCredentialHeaderValue, safeAzureWebSocketHeaders } from '../../internal/azure';
import { assertBedrockWebSocketOrigin } from '../../internal/bedrock';
import { protectWebSocketOptionsFromCredentialRedirects } from '../../internal/ws';
import type { AzureOpenAI } from '../../index';
Expand Down Expand Up @@ -67,17 +68,22 @@ export class OpenAIRealtimeWS extends OpenAIRealtimeEmitter {
this.url = buildRealtimeURL(client, props);
assertTrustedRealtimeURL(client, this.url);
assertBedrockWebSocketOrigin(client, this.url);
const azure = isAzure(client);
const headers = {
...props.options?.headers,
...(isAzure(client) && !props.__resolvedApiKey ? {} : { Authorization: `Bearer ${client.apiKey}` }),
...(azure && !props.__resolvedApiKey
? {}
: {
Authorization: `Bearer ${azure ? safeAzureCredentialHeaderValue(client.apiKey) : client.apiKey}`,
}),
'OpenAI-Beta': 'realtime=v1',
};

this.socket = new WS.WebSocket(
this.url,
protectWebSocketOptionsFromCredentialRedirects({
...props.options,
headers,
headers: azure ? safeAzureWebSocketHeaders(headers) : headers,
}),
);

Expand Down
12 changes: 9 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ import {
} from './resources/chat/completions/completions';
import { type Fetch } from './internal/builtin-types';
import { isRunningInBrowser } from './internal/detect-platform';
import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers';
import { HeadersLike, NullableHeaders, buildHeaders, captureAzureHeaders } from './internal/headers';
import { configureProvider, type Provider, type ProviderRuntime } from './internal/provider';
import { FinalRequestOptions, RequestOptions } from './internal/request-options';
import { readEnv } from './internal/utils/env';
Expand Down Expand Up @@ -1777,6 +1777,7 @@ export class OpenAI {
x509Timeout: number | undefined;
x509Tenant?: { organization: string | null; project: string | null } | undefined;
}): Promise<Headers> {
const azureRequestHeaders = captureAzureHeaders(this, options);
let idempotencyHeaders: HeadersLike = {};
if (this.idempotencyHeader && method !== 'get') {
if (!options.idempotencyKey) options.idempotencyKey = this.defaultIdempotencyKey();
Expand All @@ -1799,10 +1800,15 @@ export class OpenAI {
},
this._provider || this.#x509Authentication?.isPlanningRequest()
? undefined
: await this.authHeaders(options, options.__security ?? { bearerAuth: true }),
: azureRequestHeaders
? await azureRequestHeaders.authenticate(
this.authHeaders,
options.__security ?? { bearerAuth: true },
)
: await this.authHeaders(options, options.__security ?? { bearerAuth: true }),
x509Headers?.defaultHeaders ?? this._options.defaultHeaders,
bodyHeaders,
x509Headers?.requestHeaders ?? options.headers,
x509Headers?.requestHeaders ?? (azureRequestHeaders ? azureRequestHeaders.headers() : options.headers),
]);

if (!this._provider && !this.#x509Authentication?.isPlanningRequest()) {
Expand Down
84 changes: 84 additions & 0 deletions src/internal/azure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/** 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.');
}
}
}

/** Coerces and validates an Azure credential without exposing errors from caller-defined hooks. */
export function safeAzureCredentialHeaderValue(value: unknown): string {
let credential: string;
try {
credential = String(value);
} catch {
throw new TypeError('Azure OpenAI credential contains an invalid HTTP header value.');
}
assertAzureCredentialHeaderValue(credential);
return credential;
}

/** 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);
try {
if (Array.isArray(value)) {
const { length } = value;
if (!Number.isSafeInteger(length) || length < 0 || length > 1024) {
throw new TypeError('Azure OpenAI credential contains an invalid HTTP header value.');
}
const snapshot: unknown[] = [];
for (let index = 0; index < length; index += 1) {
const entry = value[index];
if (entry === null || entry === undefined) {
snapshot.push(entry);
continue;
}
snapshot.push(safeAzureCredentialHeaderValue(entry));
}
safeHeaders.set(name, snapshot);
} else {
safeHeaders.set(name, safeAzureCredentialHeaderValue(value));
}
} catch {
throw new TypeError('Azure OpenAI credential contains an invalid HTTP header value.');
}
}
return Object.fromEntries(safeHeaders) as Headers;
}
Loading
Loading