docs/architecture/security-model.md
This is the canonical repository-owned threat model for Codex Security scans of
the OpenAI Python SDK. For a pull-request scan, resolve this document from the
trusted base or another pinned protected revision, never from the candidate
revision being judged; if that trusted revision has no model, use separately
pinned protected scan policy rather than candidate text. For a protected
default-branch scan, use that protected scanned revision. SECURITY.md
remains the authority for coordinated disclosure instructions.
The repository publishes the official openai Python library. It is a
caller-owned client library, not a hosted multi-tenant service: application
code constructs synchronous or asynchronous clients, supplies credentials and
request data, sends HTTPS or WebSocket requests to an API endpoint, and parses
JSON, SSE, or WebSocket responses into SDK types. The package requires Python
3.10+ and has optional Realtime, voice, aiohttp, and Bedrock dependencies
(README.md:6, pyproject.toml:11,
pyproject.toml:42).
Most API resources and types are generated from the OpenAPI schema. The security-relevant handwritten surfaces are the shared transport and parsing code, authentication providers, webhook verification, provider integrations, local helpers, dependency/build policy, and release automation (AGENTS.md:3, src/openai/_client.py:157, src/openai/_base_client.py:517).
| Component | Role | Evidence |
|---|---|---|
OpenAI / AsyncOpenAI clients | Resolve credentials, endpoint configuration, headers, retries, and transports. | src/openai/_client.py:157, src/openai/_client.py:247 |
| Legacy module-level client | Selects, constructs, caches, and resets an OpenAI, Azure, or Bedrock client from module globals and environment state. | src/openai/init.py:362, src/openai/init.py:387, src/openai/init.py:404, src/openai/init.py:439 |
| Shared base client | Serializes caller data, builds HTTP requests, processes responses, and manages retries. | src/openai/_base_client.py:517, src/openai/_base_client.py:672 |
| SDK-owned default HTTP transports | Provide HTTPX-backed sync/async defaults and the optional vendored aiohttp backend, including redirect, proxy, and TLS behavior. | src/openai/_base_client.py:863, src/openai/_base_client.py:1457, src/openai/_base_client.py:1478, src/openai/_vendor/httpx_aiohttp/transport.py:100 |
| Streaming and authenticated WebSockets | Incrementally decode SSE and exchange authenticated current or beta Realtime/Responses WebSocket messages through distinct sync and async connection paths. | src/openai/_streaming.py:53, src/openai/resources/realtime/realtime.py:683, src/openai/resources/realtime/realtime.py:1153, src/openai/resources/responses/responses.py:4410, src/openai/resources/responses/responses.py:4855, src/openai/resources/beta/realtime/realtime.py:355, src/openai/resources/beta/realtime/realtime.py:540, src/openai/resources/beta/responses/responses.py:4515, src/openai/resources/beta/responses/responses.py:4960 |
| Webhooks | Verify raw inbound webhook bytes before parsing a typed event. | src/openai/resources/webhooks/webhooks.py:18, src/openai/lib/_webhooks.py:13 |
| Workload identity | Obtain local or metadata subject tokens and exchange them for OpenAI bearer tokens. | src/openai/auth/_workload.py:78, src/openai/auth/_x509.py:25 |
| CI and publication | Test PR and branch code, run CodeQL, perform the privileged monthly policy assessment, build distributions, and publish through protected release paths. | .github/workflows/ci.yml:18, .github/workflows/codeql.yml:15, .github/workflows/python-version-review.yml:15, .github/workflows/publish-pypi.yml:8 |
flowchart LR
app[Caller-owned Python process] -->|credentials, request data, files| sdk[OpenAI Python SDK]
sdk -->|HTTPS / WSS| api[OpenAI, Azure, or Bedrock API]
api -->|JSON, SSE, WebSocket frames| sdk
webhook[Webhook sender] -->|raw payload and headers| verify[Webhook verifier]
host[Local file or cloud metadata identity] -->|subject token| auth[Token exchange]
pr[PR checkout code] -->|workflow-specific permissions| ci[CI runner]
main[Protected main/release workflow] -->|artifact boundary| publish[PyPI publish job]
| Deployment or workflow | Resource or capability | Configuration and precedence | Safe effective value or location | Readers, writers, or recipients | Enforcing control | Evidence or unknowns |
|---|---|---|---|---|---|---|
| Default OpenAI client | API, admin, or ambient Authorization credential | Credential gate: explicit API/admin argument, then OPENAI_API_KEY / OPENAI_ADMIN_KEY; ambient OPENAI_CUSTOM_HEADERS authorization alone does not satisfy that gate. For ordinary non-X.509, non-provider clients, ambient, constructor-default, and per-request authorization mappings can override generated or stored values for the same exact key; case-variant names can coexist until HTTP header normalization, so every variant remains credential-bearing. | Secret remains in process memory. For identical mapping keys, effective request merge order is generated bearer auth, then stored default/ambient authorization, then per-request authorization; differently cased keys are not collapsed by that mapping merge. | Selected API destination. | Missing API/admin/workload credentials fail; per-operation security flags select ordinary versus admin auth; ordinary copies preserve ambient authorization, provider transitions clear inherited custom headers, and X.509 construction/transitions filter ambient authorization case-insensitively unless explicitly replaced. | src/openai/_client.py:247, src/openai/_client.py:261, src/openai/_client.py:310, src/openai/_client.py:318, src/openai/_client.py:589, src/openai/_client.py:635, src/openai/_base_client.py:475, src/openai/_base_client.py:2262, src/openai/_client.py:715, src/openai/_client.py:808, src/openai/auth/_x509.py:281 |
| Legacy module-level client | Global/environment-selected OpenAI, Azure, or Bedrock credential and destination | Module api_type starts from OPENAI_API_TYPE. _load_client() fills unset Azure endpoint/version globals from environment; only when api_type is unset, it rejects OPENAI_API_KEY in the environment together with any of: module azure_endpoint or AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_OPENAI_AD_TOKEN, module azure_ad_token, or module azure_ad_token_provider; module api_key and api_version are not ambiguity signals. It infers Azure when an Azure signal exists and OpenAI otherwise; exact api_type == "azure" or "amazon-bedrock" use dedicated branches, while explicit "openai" and any other non-None runtime value fall through to _ModuleClient. | Selected _client instance is cached until _reset_client(); _ModuleClient property-backed common globals such as API/admin keys remain live, while Azure endpoint/version/AAD and non-property provider inputs are constructor-captured; inferred api_type and environment-filled Azure globals persist unless changed independently. | OpenAI, Azure, or Bedrock constructor and destination selected by module state. | The exact OpenAI-environment-key plus enumerated Azure/AAD-signal ambiguity fails; exact Azure/Bedrock provider branches and OpenAI/fallback branch receive their module arguments; reset clears only the cached _client instance. | src/openai/init.py:163, src/openai/init.py:183, src/openai/init.py:339, src/openai/init.py:343, src/openai/init.py:347, src/openai/init.py:351, src/openai/init.py:362, src/openai/init.py:368, src/openai/init.py:374, src/openai/init.py:379, src/openai/init.py:387, src/openai/init.py:404, src/openai/init.py:420, src/openai/init.py:436, src/openai/init.py:439, src/openai/lib/azure.py:356, src/openai/lib/bedrock.py:478 |
| Default routing | API origin | Explicit base_url, then OPENAI_BASE_URL, then default. | https://api.openai.com/v1 by default. | HTTP transport and remote API. | Caller controls non-X.509 overrides; relative resource paths merge into the configured base URL. | src/openai/_client.py:299, src/openai/_base_client.py:501 |
| SDK-owned default HTTP transport | Redirect, proxy, TLS, and request-hook execution for default HTTPX or optional aiohttp clients | SDK constructors choose HTTPX-backed defaults unless the caller supplies http_client; selecting SDK-provided DefaultAioHttpClient uses the vendored aiohttp transport when no caller-supplied inner transport is provided, keeping that implementation SDK-owned. | Default variants set follow_redirects=True; caller-implemented clients, proxies, TLS settings, and transports remain caller-owned. | Redirect target, proxy, TLS peer, and request/response hook path. | Default transport variants and per-request follow_redirects options determine whether credentials traverse a destination change; the aiohttp transport performs its own TLS/proxy request and disables aiohttp-native redirects so the HTTPX-compatible layer owns redirect handling. | src/openai/_base_client.py:863, src/openai/_base_client.py:1069, src/openai/_base_client.py:1457, src/openai/_base_client.py:1478, src/openai/_vendor/httpx_aiohttp/client.py:16, src/openai/_vendor/httpx_aiohttp/client.py:24, src/openai/_vendor/httpx_aiohttp/transport.py:100, src/openai/_vendor/httpx_aiohttp/transport.py:170, src/openai/_vendor/httpx_aiohttp/transport.py:179 |
| Data residency | Regional API origin | data_residency selects a fixed mapping and cannot combine with explicit endpoint/provider modes. | Regional HTTPS endpoint selected by SDK mapping. | Remote API. | Conflicting routing modes raise before request construction. | src/openai/_data_residency.py:12 |
| Azure OpenAI | Azure endpoint, API key, or AAD token | Explicit Azure credential wins; otherwise AZURE_OPENAI_AD_TOKEN precedes AZURE_OPENAI_API_KEY. Explicit base_url and azure_endpoint are mutually exclusive; WebSocket routing is explicit or derived from the configured HTTP base. | azure_endpoint mode derives an Azure /openai base; explicit HTTP or WebSocket bases remain caller-owned. Secrets remain in process memory. | Azure HTTP or WebSocket peer. | Auth modes are mutually exclusive, required endpoint/version checks fail closed, an API-key redirect hook strips the key before a cross-origin redirected request, and Azure WebSocket cross-origin redirects are rejected. | src/openai/lib/azure.py:69, src/openai/lib/azure.py:90, src/openai/lib/azure.py:297, src/openai/lib/azure.py:319, src/openai/lib/azure.py:440, src/openai/lib/azure.py:507, src/openai/lib/_azure_websocket.py:8 |
| Bedrock provider | Bearer token or AWS credential-chain authority | bedrock(...) selects one bearer or AWS mode, resolves region from explicit/environment/profile input, and derives a default endpoint or accepts a caller-owned custom base. | Default or recognized canonical endpoints are region-bound HTTPS; explicit custom bases remain caller-owned. Bearer or SigV4 credentials remain in process memory. | Bedrock endpoint. | Ambiguous auth modes fail; recognized canonical endpoints receive HTTPS/region checks; bearer auth is same-origin; SigV4 requires replayable bodies and disables automatic redirects. | src/openai/providers/bedrock.py:77, src/openai/providers/bedrock.py:83, src/openai/providers/bedrock.py:132, src/openai/providers/bedrock.py:142, src/openai/providers/bedrock.py:451, src/openai/lib/_bedrock_auth.py:85 |
| X.509 workload identity | mTLS API and token exchange | X.509 identity selects mTLS default; caller supplies certificate through its HTTP transport. | API defaults to https://mtls.api.openai.com/v1; exchange is pinned to https://mtls.auth.openai.com/oauth/token. | OpenAI mTLS API and auth service. | HTTPS, origin, Host, TLS authority, credential, and authorization checks; token-exchange redirects are disabled. | src/openai/auth/_x509.py:25, src/openai/auth/_x509.py:49, src/openai/auth/_x509.py:97 |
| Subject-token workload identity | Local or metadata subject token and token-exchange destination | Provider callback; built-ins read Kubernetes token file or call Azure/GCP metadata endpoints; WorkloadIdentityAuth defaults to https://auth.openai.com/oauth/token, while direct callers may supply token_exchange_url, and posts the raw subject token to that configured recipient. | Kubernetes defaults to /var/run/secrets/kubernetes.io/serviceaccount/token; metadata hosts are fixed by helper; raw subject token enters the exchange JSON body and exchanged bearer is cached in memory. | Local host identity source, configured token-exchange recipient, then selected API destination. | Default exchange URL, redirects disabled on the exchange client, bounded timeout, token-response validation, and in-memory token cache. | src/openai/auth/init.py:6, src/openai/auth/_workload.py:17, src/openai/auth/_workload.py:78, src/openai/auth/_workload.py:128, src/openai/auth/_workload.py:181, src/openai/auth/_workload.py:220, src/openai/auth/_workload.py:340, src/openai/auth/_workload.py:345, src/openai/auth/_workload.py:355, src/openai/auth/_workload.py:366, src/openai/auth/_workload.py:369 |
| Webhook consumer | Webhook secret and raw inbound bytes | Explicit secret, then client webhook_secret, which may come from OPENAI_WEBHOOK_SECRET. | Secret remains in process memory; payload and headers are caller-supplied bytes. | SDK verifier and caller application. | Timestamp tolerance is a bounded-age check, not uniqueness or replay prevention; HMAC-SHA256 and constant-time comparison authenticate bytes; unwrap verifies before JSON parsing; caller owns webhook-id deduplication. | src/openai/_client.py:281, src/openai/resources/webhooks/webhooks.py:18, src/openai/lib/_webhooks.py:13, src/openai/lib/_webhooks.py:20, src/openai/lib/_webhooks.py:33, src/openai/lib/_webhooks.py:57 |
| File upload | Caller filesystem read | Caller passes bytes, streams, or PathLike values. | Caller-selected path or already-opened stream. | Local OS and remote API. | Local OS permissions and explicit caller invocation; SDK does not discover files automatically. | src/openai/_files.py:25, src/openai/_files.py:65 |
| HTTP/SSE response | Remote response bytes | Remote endpoint selected by caller configuration; streaming uses incremental decoders. | Remote JSON, SSE lines, or events in process memory. | SDK model parser and caller application. | Incremental SSE handling and finally response cleanup; large legitimate payloads are supported without arbitrary fixed rejection limits. | src/openai/_streaming.py:53, src/openai/_streaming.py:109, AGENTS.md:112 |
| Current and beta Realtime/Responses WebSockets | WebSocket destination and client auth headers | All current and beta paths use explicit websocket_base_url or transform the configured HTTP base to a WebSocket scheme, then merge client auth with caller extra headers. Only Realtime branches into Azure _configure_realtime for api-key or AAD bearer routing; Responses uses inherited auth_headers directly. Beta Realtime also adds OpenAI-Beta: realtime=v1 before caller-header override. | Caller-selected WebSocket origin with /realtime or /responses; current and beta sync paths call websockets.sync.client.connect directly, while current and beta async paths use the SDK same-origin redirect wrapper, with async Azure Realtime using its Azure-specific wrapper. | WebSocket peer and any redirect target. | Caller owns explicit base/options; sync direct-connect and async same-origin redirect paths are distinct credential-routing surfaces, Azure async Realtime rejects cross-origin redirects, Responses keeps the generic client-auth path, and beta Realtime's extra header remains separately reviewable. | src/openai/resources/realtime/realtime.py:683, src/openai/resources/realtime/realtime.py:693, src/openai/resources/realtime/realtime.py:1153, src/openai/resources/responses/responses.py:4410, src/openai/resources/responses/responses.py:4429, src/openai/resources/responses/responses.py:4855, src/openai/resources/responses/responses.py:4874, src/openai/resources/beta/realtime/realtime.py:355, src/openai/resources/beta/realtime/realtime.py:382, src/openai/resources/beta/realtime/realtime.py:540, src/openai/resources/beta/responses/responses.py:4515, src/openai/resources/beta/responses/responses.py:4960, src/openai/_client.py:611, src/openai/_client.py:1356, src/openai/lib/azure.py:500, src/openai/lib/azure.py:854, src/openai/lib/_websocket.py:12, src/openai/lib/_azure_websocket.py:8 |
| Package build and PyPI publish | Executable checkout code, artifacts, and OIDC publication | scripts/build invokes the locked build; artifacts pass from build job to upload-only job. | Build artifact; no long-lived PyPI token. | CI runner, artifact store, PyPI. | Locked/provenance-checked build requirements; no OIDC in build; id-token: write only in the publish job. | scripts/build:1, pyproject.toml:71, .github/workflows/publish-pypi.yml:8, .github/workflows/publish-pypi.yml:40 |
| Published dependency supply chain | Runtime, optional, and build dependency execution | pyproject.toml declares runtime/optional dependencies and the Hatch build backend; uv.lock records registry artifacts and hashes for repository builds. | Published dependency package or build backend executed under installer/runtime authority. | Package builders and SDK consumers. | Locked repository builds and provenance checks reduce CI confusion, but a malicious or provenance-confused dependency published through an ordinary trusted release remains a separate consumer boundary. | pyproject.toml:11, pyproject.toml:42, pyproject.toml:71, pyproject.toml:108, uv.lock:46, .github/workflows/ci.yml:37 |
ci.yml pull-request jobs | PR checkout execution | PR code and config are checked out and run in CI. | Tracked executable files from the PR checkout. | Read-only CI runner. | Workflow permissions are read-only, checkout credentials are not persisted, and dependency/build provenance is checked before installation. | .github/workflows/ci.yml:18, .github/workflows/ci.yml:33, .github/workflows/ci.yml:37 |
ci.yml main-branch examples job | Merged executable examples and OPENAI_API_KEY | On a same-repository push to main, the job selects environment: ci, checks out the merged revision, installs locked dependencies, and runs examples/demo.py and examples/async_demo.py with the secret in each step environment. | API key is present in the example process environment after merge; whether ci has host-side protection is external configuration. | Merged example code, dependencies, API destination, and CI runner. | Main/repository trigger condition, no persisted checkout credentials, locked dependency install, and environment selection; do not classify this as the read-only PR-job boundary. | .github/workflows/ci.yml:218, .github/workflows/ci.yml:221, .github/workflows/ci.yml:224, .github/workflows/ci.yml:227, .github/workflows/ci.yml:240, .github/workflows/ci.yml:244 |
| CodeQL on a same-repository PR | Candidate source plus security-result write token | pull_request against main checks out candidate source, which pinned CodeQL actions process without a repository run or build step. | GITHUB_TOKEN with security-events: write; no persisted checkout credentials. | CodeQL analyzer/upload path and GitHub security-events API. | This is not the read-only ci.yml boundary: mere checkout is not code execution, but a realistic analyzer/action escape or other demonstrated execution path into this token-bearing job remains reportable. | .github/workflows/codeql.yml:7, .github/workflows/codeql.yml:15, .github/workflows/codeql.yml:27, .github/workflows/codeql.yml:32 |
| Monthly Python version assessment | External lifecycle data, OpenAI key, agent output, and issue publication | Scheduled workflow downloads CPython/PyPI data, passes OPENAI_API_KEY to the pinned assessment action, runs Codex as an unprivileged user through the action's isolation boundary, marker/size-checks output, appends it to the step summary, and copies action-required output for a separate issue publisher. | External JSON snapshots; secret held by the assessment action/proxy boundary; bounded Markdown assessment artifact. | Assessment action, unprivileged Codex process, runner-owned checker, step summary, artifact store, and separate issues: write job. | Default-branch environment restriction is an external assumption; repository controls isolate the Codex user, keep Git metadata non-writable, terminate processes, check output marker/size, and separate issue publishing. Those checks do not establish semantic safety or redaction. | .github/workflows/python-version-review.yml:19, .github/workflows/python-version-review.yml:32, .github/workflows/python-version-review.yml:58, .github/workflows/python-version-review.yml:94, .github/workflows/python-version-review.yml:104, .github/workflows/python-version-review.yml:125, .github/workflows/python-version-review.yml:160, .github/workflows/python-version-review.yml:171 |
| Castiron trusted reporter and publishers | Candidate Git objects, trusted report, statuses, and PR comments | A workflow_run handler from main checks out the trusted reporter, computes over current PR Git objects, then separate jobs publish statuses and comments. | Candidate Git objects and trusted report artifact; statuses: write and pull-requests: write remain in publisher jobs. | Main reporter, GitHub status API, and PR comment API. | Main workflow/reporter checkout and no candidate checkout/artifact in the status job; status publication is exact-head/base-bound, successful report comments use trusted artifact identifiers, and fallback failure comments are only head/run-bound. | .github/workflows/castiron-custom-code-comment.yml:5, .github/workflows/castiron-custom-code-comment.yml:34, .github/workflows/castiron-custom-code-comment.yml:40, .github/workflows/castiron-custom-code-comment.yml:116, .github/workflows/castiron-custom-code-comment.yml:128, .github/workflows/castiron-custom-code-comment.yml:138, .github/workflows/castiron-custom-code-comment.yml:174, .github/workflows/castiron-custom-code-comment.yml:191, .github/workflows/castiron-custom-code-comment.yml:217 |
| Protected release workflows | Release private key, generated GitHub App write token, and publication authority | Main-only release job passes OPENAI_SDKS_APP_PRIVATE_KEY into pinned actions/create-github-app-token; its generated token is passed to pinned release-please with contents, issues, and pull-request write access. Build and upload-only PyPI publish remain separate jobs. | Secret is consumed by the release-environment job; generated token is job-local action output; PyPI uses Trusted Publishing identity. Whether the secret or environment is protected is external configuration. | Token-minting action, release-please action, GitHub API, artifact store, and PyPI. | Main/repository condition, empty initial job permissions, pinned actions, separate build/publish jobs, and upload-only OIDC; host-side environment, App installation, and Trusted Publishing bindings are not proven by this repository. | .github/workflows/create-releases.yml:4, .github/workflows/create-releases.yml:8, .github/workflows/create-releases.yml:19, .github/workflows/create-releases.yml:23, .github/workflows/create-releases.yml:28, .github/workflows/publish-pypi.yml:40 |
Authorization headers, webhook secrets,
subject tokens, exchanged bearer tokens, provider credentials, and release
credentials from reaching unintended recipients
(src/openai/_client.py:247,
src/openai/_client.py:310,
src/openai/auth/_workload.py:220).APIError.body diagnostics may remain
(AGENTS.md:26,
src/openai/_utils/_logs.py:10,
src/openai/_exceptions.py:46)..env files, and other
credentials out of tracked repository contents, history, review diffs,
checkouts, and artifacts. Clearly fake or sanitized examples and fixtures may
remain (AGENTS.md:22, AGENTS.md:26).base_url, websocket_base_url, a file path, or an HTTP client does not
gain a new SDK privilege merely because the SDK uses that value
(src/openai/_client.py:157,
src/openai/_client.py:299).api_type starts from
OPENAI_API_TYPE; _load_client() fills unset Azure endpoint/version globals
from environment. When api_type is unset, it
rejects OPENAI_API_KEY in the environment together with module
azure_endpoint/AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY,
AZURE_OPENAI_AD_TOKEN, module azure_ad_token, or module
azure_ad_token_provider; module api_key and api_version are not
ambiguity signals. Exact "azure" and "amazon-bedrock" values use
dedicated branches; with api_type unset it infers Azure when an enumerated
Azure/AAD signal exists and otherwise defaults to OpenAI, while explicit
"openai" and any other non-None runtime value fall through to
_ModuleClient, and
caches that client until _reset_client(). The reset clears only _client;
_ModuleClient property-backed common globals such as API/admin keys remain
live, while Azure endpoint/version/AAD and non-property provider inputs are
captured by the constructed client; inferred api_type or environment-filled
Azure globals persist unless changed independently. Regressions in selection,
ambiguity detection, argument forwarding, or those distinct cache/global
behaviors can route credentials to the wrong provider
(src/openai/init.py:362,
src/openai/init.py:368,
src/openai/init.py:374,
src/openai/init.py:379,
src/openai/init.py:387,
src/openai/init.py:404,
src/openai/init.py:436,
src/openai/init.py:439).DefaultAioHttpClient are distinct from caller-supplied transports. They
default to following redirects; absent a caller-supplied inner transport, the
vendored aiohttp transport owns its
TLS/proxy request path and disables aiohttp-native redirects so the
HTTPX-compatible layer handles them. Regressions in those default paths can
change whether API, admin, or ambient authorization crosses an origin; a
caller-implemented client or transport, proxy, or TLS configuration remains
caller-owned; selecting SDK-provided DefaultAioHttpClient does not move its
implementation out of the SDK-owned boundary
(src/openai/_base_client.py:863,
src/openai/_base_client.py:1457,
src/openai/_base_client.py:1478,
src/openai/_vendor/httpx_aiohttp/transport.py:100,
src/openai/_vendor/httpx_aiohttp/transport.py:170,
src/openai/_vendor/httpx_aiohttp/transport.py:179).websockets.sync.client.connect directly, while each current or beta async
path uses the SDK same-origin redirect wrapper. All merge client auth with
caller extra headers; only Realtime invokes Azure _configure_realtime for
api-key or AAD bearer routing, while Responses uses inherited
auth_headers directly. Beta Realtime adds OpenAI-Beta: realtime=v1
before caller-header override. Async Azure Realtime also uses its
Azure-specific redirect wrapper. Review sync and async paths independently
because their redirect enforcement is not shared
(src/openai/resources/realtime/realtime.py:683,
src/openai/resources/realtime/realtime.py:693,
src/openai/resources/realtime/realtime.py:1153,
src/openai/resources/responses/responses.py:4410,
src/openai/resources/responses/responses.py:4855,
src/openai/resources/beta/realtime/realtime.py:355,
src/openai/resources/beta/realtime/realtime.py:382,
src/openai/resources/beta/realtime/realtime.py:540,
src/openai/resources/beta/responses/responses.py:4515,
src/openai/resources/beta/responses/responses.py:4960,
src/openai/lib/_websocket.py:12,
src/openai/lib/_azure_websocket.py:8).WorkloadIdentityAuth defaults to the OpenAI auth exchange but direct
callers can configure token_exchange_url; it sends the token in JSON to
that configured recipient with redirects disabled. Changing the default
destination or redirect behavior is a separate credential-routing boundary
before the exchanged bearer reaches the API
(src/openai/auth/_workload.py:17,
src/openai/auth/_workload.py:78,
src/openai/auth/init.py:6,
src/openai/auth/_workload.py:128,
src/openai/auth/_workload.py:345,
src/openai/auth/_workload.py:355,
src/openai/auth/_workload.py:366,
src/openai/auth/_workload.py:369).ci.yml examples job is a distinct post-merge boundary, not part of the
read-only pull-request statement: on a same-repository push to main, it
checks out the merged revision and runs examples/demo.py and
examples/async_demo.py with OPENAI_API_KEY in each process environment.
The YAML selects environment: ci, but whether that environment actually has
host-side protection or approvals must be verified separately
(.github/workflows/ci.yml:218,
.github/workflows/ci.yml:221,
.github/workflows/ci.yml:224,
.github/workflows/ci.yml:240,
.github/workflows/ci.yml:244).ci.yml. CodeQL is a distinct
same-repository PR boundary because pinned analyzer actions process candidate
source while the job token has security-events: write. The workflow does
not run repository scripts merely by checking out the source; scans must
investigate a realistic analyzer/action escape or another demonstrated
execution path before treating token misuse as reportable
(.github/workflows/codeql.yml:7,
.github/workflows/codeql.yml:15,
.github/workflows/codeql.yml:27,
.github/workflows/codeql.yml:32).OPENAI_API_KEY. Marker/size-checked output is appended to the
step summary, and action-required output becomes the intended issue body for
a separate issues: write publisher. Those checks do not prove semantic
safety or redaction. Regressions that expose the key, weaken workspace
ownership, leak sensitive output to the summary, escape the intended issue
body into publisher commands/metadata/targets, obtain broader issue-write
authority, or collapse job separation remain reportable boundaries
(.github/workflows/python-version-review.yml:58,
.github/workflows/python-version-review.yml:94,
.github/workflows/python-version-review.yml:104,
.github/workflows/python-version-review.yml:125,
.github/workflows/python-version-review.yml:160,
.github/workflows/python-version-review.yml:171).workflow_run handler is another distinct privileged boundary:
main's trusted reporter evaluates candidate Git objects, then publisher jobs
hold statuses: write and pull-requests: write. Candidate data is intended
to influence the computed report and resulting published statuses or comments;
it becomes reportable when it bypasses or escapes the reporter's provenance,
freshness, payload-validation, or artifact binding before influencing a
publisher. Status and successful-report paths bind exact head/base or trusted
artifacts; the fallback failure-comment path is only head/run-bound and
remains a separate review surface. Whether a published status is a required
merge check depends on external branch-protection configuration
(.github/workflows/castiron-custom-code-comment.yml:5,
.github/workflows/castiron-custom-code-comment.yml:40,
.github/workflows/castiron-custom-code-comment.yml:116,
.github/workflows/castiron-custom-code-comment.yml:138,
.github/workflows/castiron-custom-code-comment.yml:174).OPENAI_SDKS_APP_PRIVATE_KEY secret referenced by its release-environment job
is received by pinned
actions/create-github-app-token, and its generated contents/issues/pull-
requests write token is then received by pinned release-please. A
regression in either action pin, input, output, permission, or recipient is
reportable independently from PyPI's later upload-only OIDC boundary
(.github/workflows/create-releases.yml:8,
.github/workflows/create-releases.yml:19,
.github/workflows/create-releases.yml:23,
.github/workflows/create-releases.yml:28,
.github/workflows/publish-pypi.yml:40).A reportable SDK finding requires a realistic new capability across an actual
boundary: independently mutable lower-trust input crossing a parser/evaluator
boundary into a sensitive sink; untrusted runtime, API, network, webhook, or
metadata data reaching credentials, local files, code execution, or caller
security decisions; unredacted sensitive material reaching logs, exceptions,
snapshots, or test output; real credentials, private keys, bearer tokens, or
.env files reaching tracked repository contents, history, review diffs,
checkouts, or artifacts; PR-controlled code reaching protected CI/release
credentials, write-capable tokens, or publication authority; or published
dependency, build-backend, or install-script compromise reaching package
consumers through an ordinary trusted release. Ordinary authorized SDK behavior,
self-only effects within authority the caller or PR author already has, and
keyword matches in clearly fake or sanitized tracked examples/fixtures are not
findings by themselves. Safe or sanitized caller-visible APIError.body
diagnostics are not findings by themselves.
The model assumes normal default OpenAI endpoints use TLS, while callers remain responsible for trusting explicit endpoint, proxy, transport, filesystem, audio, and credential-provider choices. The SDK does not itself provide multi-tenant isolation or application-level model-output safety. Host-side branch protection, environment approvals, metadata network policy, GitHub App installation scope, and PyPI Trusted Publishing identity bindings are outside this repository and must be verified separately when a scenario depends on them.
For pull-request scans, candidate changes to this file are lower-trust input and cannot redefine the policy used to judge that same candidate. Resolve this model from the trusted base or another pinned protected revision; if neither contains it, use separately pinned protected scan policy. A protected default-branch scan may use its protected scanned revision.
These are reusable hypotheses and review guidance, not confirmed vulnerabilities.
| Priority | Scenario and capability gain | Prerequisites | Impact | Existing controls | Mitigation | Evidence |
|---|---|---|---|---|---|---|
| High | Credential misrouting sends API, admin, ambient Authorization, provider, or WebSocket auth to an unintended origin. | Independently mutable lower-trust input must influence a destination after the caller chose trusted configuration, or an SDK-owned auth/transport mode must lose its binding. | Credential or caller-data disclosure. | Default OpenAI origin; ambient-authorization override/mode-switch filtering; default transport redirect behavior; residency conflict checks; X.509 origin/Host/TLS/auth checks; provider-specific controls. | Keep destination configuration privileged; preserve and test ambient-header, redirect, and binding checks in SDK-owned auth/transport modes. | src/openai/_client.py:299, src/openai/_client.py:310, src/openai/_client.py:715, src/openai/_base_client.py:863, src/openai/_base_client.py:1478, src/openai/_data_residency.py:12, src/openai/auth/_x509.py:49 |
| High | Subject-token exchange routing regression sends a raw host-identity token to an unintended recipient. | A regression changes the default OpenAI auth exchange destination, forwards the JSON body across a redirect, or otherwise loses the no-redirect binding; a direct caller intentionally supplying a different token_exchange_url is insufficient by itself. | Kubernetes, Azure, or GCP subject-token disclosure and possible workload-identity abuse. | Default https://auth.openai.com/oauth/token, caller-configurable exchange recipient, raw token only in the exchange JSON body, redirects disabled, bounded timeout, and response validation. | Preserve and test the default recipient, configured-recipient handling, payload contents, and no-redirect invariant independently from metadata acquisition. | src/openai/auth/init.py:6, src/openai/auth/_workload.py:17, src/openai/auth/_workload.py:340, src/openai/auth/_workload.py:345, src/openai/auth/_workload.py:355, src/openai/auth/_workload.py:366, src/openai/auth/_workload.py:369 |
| High | Legacy module-client selection or cache regression routes an OpenAI, Azure, or Bedrock credential through the wrong provider. | Mutable module globals or environment signals must cross a broken precedence, ambiguity, forwarding, or reset boundary; the caller deliberately setting a coherent provider configuration is insufficient. | Credential or caller-data disclosure to an unintended provider destination. | Exact OPENAI_API_KEY-environment plus enumerated Azure/AAD-signal ambiguity rejection, exact Azure/Bedrock branches, OpenAI/other-value fallthrough, property-backed live common globals, constructor-captured provider inputs, and _reset_client() clearing only the cached instance. | Preserve selection precedence, ambiguity checks, provider-specific forwarding, and distinct instance-cache versus global-state semantics. | src/openai/init.py:163, src/openai/init.py:183, src/openai/init.py:339, src/openai/init.py:343, src/openai/init.py:347, src/openai/init.py:351, src/openai/init.py:362, src/openai/init.py:374, src/openai/init.py:379, src/openai/init.py:387, src/openai/init.py:404, src/openai/init.py:420, src/openai/init.py:439, src/openai/lib/azure.py:356 |
| High | A current or beta Realtime/Responses WebSocket regression sends client auth to the wrong peer or redirect target. | A sync direct-connect or async wrapper path must lose its destination/header binding; caller-selected explicit WebSocket configuration alone is insufficient. | API credential disclosure; Azure api-key or AAD exposure applies to Realtime's Azure configuration path, while Responses uses inherited client auth directly. | Current/beta /realtime and /responses URL derivation, client-auth plus extra-header merge, beta Realtime header ordering, async same-origin redirect wrapper, Azure async Realtime cross-origin rejection, and Responses generic auth-header path; sync direct-connect remains a separate review surface. | Test current and beta Realtime/Responses sync/async paths separately, including redirects and their distinct auth-header routing. | src/openai/resources/realtime/realtime.py:683, src/openai/resources/realtime/realtime.py:693, src/openai/resources/realtime/realtime.py:1153, src/openai/resources/responses/responses.py:4410, src/openai/resources/responses/responses.py:4429, src/openai/resources/responses/responses.py:4855, src/openai/resources/responses/responses.py:4874, src/openai/resources/beta/realtime/realtime.py:355, src/openai/resources/beta/realtime/realtime.py:382, src/openai/resources/beta/realtime/realtime.py:540, src/openai/resources/beta/responses/responses.py:4515, src/openai/resources/beta/responses/responses.py:4960, src/openai/_client.py:611, src/openai/_client.py:1356, src/openai/lib/azure.py:500, src/openai/lib/azure.py:854, src/openai/lib/_websocket.py:12, src/openai/lib/_azure_websocket.py:8 |
| High | Azure auth precedence or endpoint/WebSocket routing regression sends an API key or AAD token to the wrong destination. | Lower-trust input must cross into SDK-owned Azure auth/routing checks, or those checks regress; caller-chosen trusted endpoint configuration alone is insufficient. | Azure credential or caller-data disclosure. | Mutually exclusive auth modes, explicit-over-environment precedence, required endpoint/version, cross-origin API-key redirect stripping, and cross-origin Azure WebSocket redirect rejection. | Preserve Azure-specific auth selection and redirect/routing tests. | src/openai/lib/azure.py:69, src/openai/lib/azure.py:90, src/openai/lib/azure.py:297, src/openai/lib/azure.py:319, src/openai/lib/azure.py:507, src/openai/lib/_azure_websocket.py:8 |
| High | Bedrock auth or signing regression sends bearer/AWS credentials to the wrong origin or signs the wrong request. | Lower-trust input must cross into SDK-owned origin/redirect/SigV4 checks, or those checks regress; caller-owned custom base configuration alone is insufficient. | AWS credential exposure or unauthorized signed requests. | Ambiguous-mode rejection; HTTPS/region validation for recognized canonical endpoints; same-origin bearer auth; replayable SigV4 body requirement; and SigV4 redirects disabled. | Preserve Bedrock-specific mode, destination, and signing invariants. | src/openai/providers/bedrock.py:77, src/openai/providers/bedrock.py:83, src/openai/providers/bedrock.py:132, src/openai/providers/bedrock.py:142, src/openai/providers/bedrock.py:451, src/openai/lib/_bedrock_auth.py:85 |
| High | PR-controlled execution reaches a protected release credential or a release action misuses the App credential handoff without successful publication. | A PR path must receive a write-capable token, protected environment secret, OIDC publication capability, or mutable artifact path beyond its intended authority; independently, a regression can expose OPENAI_SDKS_APP_PRIVATE_KEY to an unintended recipient, alter or unpin its token-minting boundary, or misuse the generated write token in release-please. | Release credential/token compromise or unauthorized repository write capability short of package publication. | Read-only CI permissions, no persisted checkout credentials, main/repository release condition, empty initial release-job permissions, pinned token-minting/release actions, separate build/publish jobs, upload-only OIDC. | Preserve job separation, pinned actions, least privilege, protected environments, token recipients, and artifact integrity. | .github/workflows/ci.yml:18, .github/workflows/ci.yml:37, .github/workflows/create-releases.yml:8, .github/workflows/create-releases.yml:19, .github/workflows/create-releases.yml:23, .github/workflows/create-releases.yml:28, .github/workflows/publish-pypi.yml:8 |
| Critical | Unauthorized release or publication authority successfully publishes a malicious package. | An attacker must cross a protected release, artifact, or upload-only OIDC boundary and complete publication; token exposure without publication remains High. | Broadly trusted distribution compromise affecting package consumers. | Separate build/publish jobs, artifact handoff, protected publication environment assumptions, and upload-only OIDC job. | Preserve artifact integrity, trusted-publishing bindings, and release separation; treat confirmed unauthorized publication as Critical. | .github/workflows/create-releases.yml:8, .github/workflows/publish-pypi.yml:8, .github/workflows/publish-pypi.yml:40 |
| High | A merged example change reaches the main-branch examples job's OPENAI_API_KEY. | Candidate-controlled executable example content is merged into main, then the same-repository push job runs that merged content with the secret; ordinary read-only PR execution alone is insufficient. | API-key disclosure or unauthorized API use from CI. | Main/repository trigger condition, environment: ci selection, no persisted checkout credentials, and locked dependency install; actual environment protection is external. | Preserve post-merge review of executable examples and verify host-side environment protection separately. | .github/workflows/ci.yml:218, .github/workflows/ci.yml:221, .github/workflows/ci.yml:224, .github/workflows/ci.yml:227, .github/workflows/ci.yml:240, .github/workflows/ci.yml:244 |
| High | Same-repository PR source causes an analyzer/action escape that repurposes CodeQL's security-events: write token. | Candidate source reaches the pinned CodeQL analyzer and a realistic escape or separately demonstrated execution path reaches the token-bearing job; mere checkout is insufficient. | Unauthorized security-event writes or any broader capability exposed by a regression in token use. | Narrow declared permissions, no persisted checkout credentials, pinned CodeQL actions, and no repository run step. | Preserve least privilege and ensure analyzer/action processing cannot exfiltrate or reuse the token outside intended security-result publication. | .github/workflows/codeql.yml:7, .github/workflows/codeql.yml:15, .github/workflows/codeql.yml:27, .github/workflows/codeql.yml:32 |
| Medium | Forged webhook is accepted, or a caller mistakes a bounded-age verified webhook for unique delivery. | Attacker controls payload/headers but not the secret and verifier is bypassed/weakened, or attacker replays the same valid signed payload within tolerance where caller-side webhook-id deduplication is absent. | Unauthorized downstream action in caller application. | HMAC-SHA256, bounded-age timestamp tolerance, constant-time comparison, and verify-before-parse unwrap; SDK verifier is stateless and does not deduplicate. | Keep verification on raw bytes before application logic, protect/rotate secrets, and deduplicate webhook IDs at the caller boundary when uniqueness matters. | src/openai/resources/webhooks/webhooks.py:18, src/openai/lib/_webhooks.py:13, src/openai/lib/_webhooks.py:20, src/openai/lib/_webhooks.py:33, src/openai/lib/_webhooks.py:57 |
| Medium | Attacker-influenced remote JSON, SSE, or WebSocket input causes parser confusion, availability pressure, or unsafe caller-visible state. | Content returned through the normal OpenAI API or a caller-selected endpoint reaches SDK parsing/buffering/cleanup paths; impact must exceed ordinary malformed-response errors. | Process availability or downstream security decision impact. | JSON-only parsing, incremental SSE decoder, response cleanup, caller-configurable timeout/cancellation. | Preserve incremental handling and cleanup for large legitimate payloads; validate before sensitive application sinks. | src/openai/_base_client.py:672, src/openai/_streaming.py:166, AGENTS.md:112 |
| Medium | Sensitive data is emitted through diagnostics. | Runtime/API/network data or secret-bearing state reaches logs, exceptions, snapshots, or test output without redaction. Safe or sanitized caller-visible APIError.body diagnostics alone are insufficient. | Credentials, customer data, or unredacted sensitive bodies become readable to unintended diagnostic recipients. | Sensitive-header log filter, repository redaction requirements, and the safe/sanitized APIError.body carve-out. | Preserve redaction at every diagnostic boundary and avoid copying unredacted sensitive bodies into logs, exceptions, snapshots, and test output. | AGENTS.md:26, src/openai/_utils/_logs.py:10, src/openai/_exceptions.py:46 |
| High | A real credential, private key, bearer token, or .env file is committed to repository contents. | Candidate or generated content contains a real secret rather than a clearly fake or sanitized fixture; code execution is not required. | Secret disclosure through history, review diffs, checkouts, or artifacts. | Repository policy forbids real credentials and .env files and requires fake/sanitized fixtures. | Remove and rotate exposed credentials, preserve secret scanning, and keep the fake-fixture carve-out narrow. | AGENTS.md:22, AGENTS.md:26 |
| Medium | Monthly review input or output escapes its intended isolation or publication boundary. | A regression lets independently mutable lifecycle/PyPI data expose the action-held key, mutate protected workspace state, leak sensitive output to the step summary, escape the bounded issue body into publisher commands/metadata/target selection, or obtain broader issues: write use. Normal publication of the marker/size-checked issue body is not itself a finding. | OpenAI key exposure or unauthorized issue content/write behavior. | Unprivileged Codex user, non-writable Git metadata, process termination, output marker/size checks, and separate issue publisher. | Preserve environment restriction, action/process isolation, output checks, redaction, and the job boundary. | .github/workflows/python-version-review.yml:19, .github/workflows/python-version-review.yml:58, .github/workflows/python-version-review.yml:94, .github/workflows/python-version-review.yml:104, .github/workflows/python-version-review.yml:125, .github/workflows/python-version-review.yml:160, .github/workflows/python-version-review.yml:171 |
| High | Castiron candidate data escapes the trusted reporter/publisher boundary and changes published commit statuses or PR comments. | Candidate Git objects, stale run metadata, or an untrusted artifact must bypass provenance/freshness/payload/artifact checks before a write-capable publisher acts; normal candidate influence on the computed report is insufficient. | Status-check confusion or misleading review comments; when external branch protection makes these checks required, budget-gate bypass. | Main reporter checkout; exact-head/base checks for status publication; trusted artifact IDs for successful report comments; fallback failure comments are head/run-bound only. | Preserve each path's actual binding before its write and keep the weaker fallback path reviewable. | .github/workflows/castiron-custom-code-comment.yml:34, .github/workflows/castiron-custom-code-comment.yml:40, .github/workflows/castiron-custom-code-comment.yml:128, .github/workflows/castiron-custom-code-comment.yml:138, .github/workflows/castiron-custom-code-comment.yml:174, .github/workflows/castiron-custom-code-comment.yml:191, .github/workflows/castiron-custom-code-comment.yml:217 |
| High | A malicious or provenance-confused published dependency compromises SDK consumers through an ordinary release. | Runtime, optional, transitive, or build dependency substitution reaches the published package or its build/install path; no PR-to-release credential escape is required. | Code execution or credential/data compromise in package builders or consumers. | Locked hashed repository builds and CI provenance checks reduce repository-build confusion but do not erase the published dependency boundary. | Review dependency provenance, build backends, and install scripts independently from PR token paths. | pyproject.toml:11, pyproject.toml:42, pyproject.toml:108, uv.lock:46, .github/workflows/ci.yml:37 |
| Medium | Host identity token misuse mints OpenAI bearer authority. | Attacker independently reads the mounted token or reaches metadata from a process that should not have that authority. | Unauthorized API calls as the workload identity. | Fixed helper endpoints/headers, bounded timeouts, token response validation, in-memory cache. | Restrict pod/file/metadata access and use workload identity only in intended runtimes. | src/openai/auth/_workload.py:78, src/openai/auth/_workload.py:128, src/openai/auth/_workload.py:283 |
| Low | Attacker-controlled application input becomes a local file upload. | Consuming application passes an attacker-chosen path while the process already has local read authority. | Local data disclosure to the configured API. | Explicit caller invocation and OS file permissions; no automatic discovery. | Validate/allowlist paths at the application boundary or pass opened streams/bytes. | src/openai/_files.py:25, src/openai/_files.py:65 |
| Low | Local audio is captured or played unexpectedly. | Application explicitly invokes optional helpers with device permission. | Self-only local privacy or nuisance effect unless a distinct application boundary is shown. | Optional dependency, explicit helper path, OS permissions. | Gate helpers behind user intent and OS permission UX. | pyproject.toml:46, src/openai/helpers/microphone.py:81 |
| Not a finding by itself | A PR changes a checked-in test, clearly fake or sanitized fixture/example, build script, or other tracked executable file and CI runs it. | PR author already controls that checkout code and no real credential is committed. | No new capability without a separate protected sink; a real committed secret remains reportable without execution. | Repository-code authority is explicit; CI boundary is evaluated at credentials and permissions. | Investigate if execution crosses into protected authority or tracked content contains a real secret. | .github/workflows/ci.yml:18, .github/workflows/ci.yml:69, AGENTS.md:22 |
Unsupported stories remain out of scope until their missing prerequisite is shown: XSS, CSRF, SQL injection, and server-side authorization failures are not SDK vulnerabilities merely because the SDK can carry application data; prompt injection in a consuming application is not an SDK finding without an SDK-owned sensitive sink; and mutable operator configuration is not attacker-controlled unless a real lower-trust path to it is established.