OWASP API10:2023 Unsafe Consumption of APIs is the risk of trusting data and connections from the APIs you call more than input from your own users. Fix it by treating every upstream response as untrusted: validate it against a strict schema, cap its size, and encode or parameterize it before it reaches a database, a browser or a shell. Set connect and read timeouts, because several popular clients have no timeout by default. Don't follow redirects blindly; keep them on an allowlist and re-check each hop. Keep TLS verification on, scope and rotate the credentials you hold for each provider, and draw the trust boundary so a compromised provider is contained.
- Data from a provider is input, not truth: validate it with the same schema strictness you apply to a user request, then encode it for its sink.
- Several clients don't time out by default: Python requests waits forever, Go's
http.Clienthas no timeout unless you set one, and httpx uses 5 seconds. - Blindly following redirects lets a compromised provider send your request and its credentials elsewhere; keep redirect targets on an allowlist.
- Never disable TLS verification (
verify=False,rejectUnauthorized: false,InsecureSkipVerify) to make an integration work. - HTTP clients have leaked credentials across redirects: requests sent the
Authorizationheader to an http URL (CVE-2018-18074) and theProxy-Authorizationheader across hosts (CVE-2023-32681); Deno's fetch keptAuthorizationacross domains (CVE-2025-21620). - A partner API is outside your trust boundary even under contract; its compromise should not become your incident.
| Setting | Value | Note | Source |
|---|---|---|---|
| Response handling | Parse into a strict schema; reject unknown fields and wrong types | Then encode or parameterize before SQL, HTML or a shell. OWASP API10 scenario 1 is stored injection from an enrichment service. | OWASP API Security Top 10 2023, API10:2023 |
| Python requests timeout | Always pass timeout=(connect, read); there is no default | "By default, requests do not time out unless a timeout value is set explicitly." Without it the call can hang for minutes. | Requests advanced usage: Timeouts (2.34.2) |
| httpx timeout | 5 seconds by default; keep a real value, never None | timeout=None disables all timeouts. Tested: httpx.Client().timeout is 5s on 0.28.1. | HTTPX timeouts documentation (0.28.1) |
Go net/http client timeout | Set http.Client{Timeout: ...}; the zero value means no timeout | Also set per-phase timeouts in the Transport for connect and TLS handshake. | Go net/http Client documentation |
| Redirects | Do not follow automatically; allowlist hosts and re-check each hop | In fetch use redirect: "manual"; in httpx follow_redirects defaults to False; cap the number of hops. | OWASP API10:2023 How To Prevent; WHATWG Fetch |
| Response size | Stream with a byte cap and abort past it | An unbounded response is a denial-of-service and memory risk. OWASP API10 lists no resource limits as a cause. | OWASP API Security Top 10 2023, API10:2023 |
| TLS verification | On, always, with hostname checks | verify=False, rejectUnauthorized: false and InsecureSkipVerify disable it; requests warns this enables MitM. | Requests SSL cert verification documentation |
| Credentials for each provider | A distinct, least-privilege, rotatable secret per integration | Scope limits the blast radius if the provider or the credential is compromised. | OWASP ASVS 5.0.0 V13 Configuration |
- Grep the code for every outbound HTTP call and confirm each sets a timeout; flag
requests.get/postwithouttimeout=, Go clients usinghttp.DefaultClientor a zeroTimeout, and any fetch without anAbortSignal. - Point an integration at a test server that returns a 200 with a 100 MB body, a 302 to a host you do not expect, a certificate for the wrong hostname, and a slow drip of one byte per second; confirm the client caps the size, refuses the redirect, rejects the certificate and times out.
- Feed each upstream response a payload with an extra field, a wrong type, a null where a value is required and an injection string, and confirm the schema rejects it before it reaches a database or a template.
- Search for
verify=False,rejectUnauthorized: falseandInsecureSkipVerifyand remove every one that is not a clearly scoped, documented test fixture. - Check which headers your client sends after a cross-host redirect, so a provider cannot harvest your
AuthorizationorProxy-Authorizationheader (the failure mode behind CVE-2018-18074, CVE-2023-32681 and CVE-2025-21620). - Keep an inventory of every third-party API you call, the credential each uses and its data classification, and alert on upstream error-rate and latency spikes.
- Applies to
- Python requests / httpx 2.34.2 / 0.28.1 tested
- Pydantic 2.14.0 tested
- Node.js fetch / undici 26.3.0 / 8.11.2 tested
- zod 4.6.5
- Go net/http Client.Timeout zero means no timeout; sensitive headers dropped on cross-host redirect
Services that call other APIs: partner integrations, payment and identity providers, enrichment and data services, and internal microservices. Library defaults below are for current versions, so pin and re-check them.
- Part of
- Workflow: Secure a feature
Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.
Learning goals and prerequisites
What you will learn
- Treat an upstream response as untrusted input and validate it before use.
- Set connect, read and total timeouts and a response size cap on every outbound call.
- Decide redirect, TLS and credential-scoping policy for each provider you call.
- Draw the trust boundary so a compromised provider cannot pivot into your system.
Before you start
Developers who integrate payment, identity, enrichment or partner APIs, and reviewers checking outbound HTTP clients.
- Make HTTP requests from server code in at least one language.
- Know the difference between input you validate and data you trust.
Refresh first: Verify webhook deliveries before acting
Go to the guided practiceOWASP API Security Top 10 2023 added API10:2023, Unsafe Consumption of APIs, because developers trust data from the APIs they integrate with more than data from users, and so apply weaker validation, transport security and resource limits to it. The data is still attacker-reachable: a provider can be compromised, can be malicious, or can simply pass through content an attacker planted earlier. OWASP's first example is an enrichment service that stores a SQL injection payload, which runs when the consuming API pulls it into its own database.
A call to another service is therefore two trust problems: the response body is untrusted input, and the connection itself needs the same transport and resource controls you would put on any network boundary. The OWASP page lists five conditions that make an API vulnerable: talking over an unencrypted channel, not validating or sanitizing third-party data, blindly following redirects, not limiting resources used to process responses, and having no timeouts.
| Control | What goes wrong without it | Where it belongs |
|---|---|---|
| Response schema validation | Injection and type confusion from upstream data | Right after the response is received |
| Timeouts | A slow or dead provider hangs your workers | Every outbound client |
| Size cap | A huge response exhausts memory | While streaming the body |
| Redirect policy | Request and credentials sent to an attacker host | The client's redirect handling |
| TLS verification | Man-in-the-middle of the integration | The client's TLS settings |
| Credential scope | A provider compromise reaches more than it should | Secret issuance and rotation |
Decide what a well-formed response looks like and reject everything else: known fields, exact types, ranges and lengths. A strict schema turns a surprising or malicious payload into a clean rejection instead of a value that flows into a query or a page. In a test with Pydantic 2.14.0, the model below rejected a lowercase symbol, a negative price and an extra field; an accepted value is safe to use because its shape is now known. Apply the same discipline in any language: pydantic in Python, zod in TypeScript, a JSON Schema validator elsewhere.
Validation is not encoding. After the value passes the schema it is still just data; parameterize it into SQL, escape it for HTML and never pass it to a shell. The injection guides at /domains/appsec/secure-coding/injection-parser-boundaries/ cover the sinks.
from pydantic import BaseModel, ConfigDict, Field, ValidationError
class Quote(BaseModel):
model_config = ConfigDict(extra="forbid") # unknown fields are rejected
symbol: str = Field(pattern=r"^[A-Z]{1,5}$")
price_cents: int = Field(ge=0, le=100_000_000)
def parse_quote(payload: dict) -> Quote:
# Raises ValidationError on a wrong type, a bad range or an unexpected field.
return Quote.model_validate(payload)A missing timeout is the easiest way to turn one slow provider into an outage: requests pile up and workers block. The defaults vary and several are dangerous. Python's requests does not time out at all unless you pass a value: its docs say "By default, requests do not time out unless a timeout value is set explicitly." Go's http.Client has a zero Timeout by default, meaning no limit. httpx defaults to 5 seconds, which is sane but worth setting deliberately. Node's fetch has no total-request timeout of its own; pass an AbortSignal.
Set a connect timeout and a read timeout, and a total budget if the client supports one. Treat a timeout as a failed call: fail the operation, don't silently use partial data.
| Client | Default timeout | How to set one |
|---|---|---|
| Python requests 2.34 | None (waits forever) | timeout=(3.05, 27) for connect and read |
| Python httpx 0.28 | 5 seconds | httpx.Timeout(5.0, connect=3.0); never None in production |
| Node.js fetch | No total-request timeout | signal: AbortSignal.timeout(5000) |
Go net/http | None (zero Timeout) | http.Client{Timeout: 10 * time.Second} plus Transport dial/TLS timeouts |
Java java.net.http | No request timeout by default | HttpRequest.newBuilder().timeout(Duration.ofSeconds(10)) |
OWASP's second example is a compromised storage provider that answers with a 308 redirect to an attacker server; an API that follows it resends sensitive data there. The fix from the How To Prevent list is blunt: "Maintain an allowlist of well-known locations integrated APIs may redirect yours to: do not blindly follow redirects." Turn automatic redirects off, inspect the Location, and only follow it when the target host is one you expect. httpx already defaults follow_redirects to False; fetch takes redirect: "manual"; set CheckRedirect in Go.
Redirects are also where client libraries have leaked credentials. Python requests sent the Authorization header to an http URL on a same-host https-to-http redirect (CVE-2018-18074, fixed in 2.20.0) and forwarded the Proxy-Authorization header to the destination on a redirect to HTTPS (CVE-2023-32681, fixed in 2.31.0). Deno's fetch kept the Authorization header when a response redirected to a different domain (CVE-2025-21620, fixed in 2.1.2). The WHATWG Fetch standard requires dropping Authorization and other sensitive headers on a cross-origin redirect, and Go's net/http drops Authorization, Cookie and WWW-Authenticate when a redirect leaves the initial domain. Keep clients patched and prefer handling redirects yourself so you control which headers are resent.
This wrapper, tested on Node.js 26.3.0, sets a timeout with AbortSignal.timeout, refuses to follow a redirect to a host that is not on the allowlist, and streams the body with a byte cap so a huge response can't exhaust memory. In the test it fetched an allowed endpoint and blocked a 302 to attacker.example.
const MAX_BYTES = 1 << 20; // 1 MiB
const ALLOWED_HOSTS = new Set(["api.example.com"]);
export async function getJson(url, { token } = {}) {
let current = new URL(url);
for (let hop = 0; hop < 5; hop++) {
if (!ALLOWED_HOSTS.has(current.hostname)) {
throw new Error(`host not allowed: ${current.hostname}`);
}
const res = await fetch(current, {
redirect: "manual", // inspect redirects instead of following them
signal: AbortSignal.timeout(5000),
headers: token ? { authorization: `Bearer ${token}` } : {},
});
if (res.status >= 300 && res.status < 400) {
const location = res.headers.get("location");
if (!location) throw new Error("redirect without Location");
current = new URL(location, current); // re-checked against the allowlist
continue;
}
if (!res.ok) throw new Error(`upstream ${res.status}`);
return await readCapped(res, MAX_BYTES);
}
throw new Error("too many redirects");
}
async function readCapped(res, max) {
const reader = res.body.getReader();
const chunks = [];
let total = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
total += value.length;
if (total > max) {
await reader.cancel();
throw new Error("response too large");
}
chunks.push(value);
}
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
}Pass the parsed JSON through a schema before any code reads a field. With zod's strictObject, an unexpected field fails, a wrong type fails, and the result is typed so downstream code can't read a field that isn't there.
import { z } from "zod";
const Quote = z
.strictObject({
symbol: z.string().regex(/^[A-Z]{1,5}$/),
priceCents: z.number().int().min(0).max(100_000_000),
});
export function parseQuote(data: unknown) {
return Quote.parse(data); // throws on unknown fields, wrong types or bad ranges
}Configure the client once: a real timeout, follow_redirects left off, connection limits, and TLS verification left on (its default). Read the body with a size cap, then validate it with the schema from earlier. In a test on httpx 0.28.1, the client reported a 3-second connect and 5-second read timeout with redirects disabled.
import httpx
from quotes import Quote # the strict Pydantic model
client = httpx.Client(
timeout=httpx.Timeout(5.0, connect=3.0), # never None in production
follow_redirects=False, # inspect redirects yourself
limits=httpx.Limits(max_connections=20),
# verify defaults to True; do not set verify=False to "make it work"
)
MAX_BYTES = 1 << 20
def get_quote(url: str, token: str) -> Quote:
with client.stream("GET", url, headers={"Authorization": f"Bearer {token}"}) as r:
if r.is_redirect:
raise RuntimeError(f"unexpected redirect to {r.headers.get('location')}")
r.raise_for_status()
body = bytearray()
for chunk in r.iter_bytes():
body += chunk
if len(body) > MAX_BYTES:
raise RuntimeError("response too large")
return Quote.model_validate_json(bytes(body))A regression test against a local server proves the policy holds: a redirect raises instead of being followed, and an oversized body is rejected. Keep it in the suite so a later refactor can't quietly re-enable redirects or drop the cap.
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import pytest
from upstream import get_quote
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/evil":
self.send_response(302)
self.send_header("Location", "https://attacker.example/")
self.end_headers()
else:
self.send_response(404)
self.end_headers()
def log_message(self, *args):
pass
def test_rejects_redirect():
server = HTTPServer(("127.0.0.1", 0), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
port = server.server_address[1]
with pytest.raises(RuntimeError, match="redirect"):
get_quote(f"http://127.0.0.1:{port}/evil", token="t")
server.shutdown()Disabling certificate verification to get past a self-signed or mismatched certificate turns the integration into an open man-in-the-middle. The requests documentation says plainly that with verify=False it "will accept any TLS certificate presented by the server, and will ignore hostname mismatches and/or expired certificates, which will make your application vulnerable to man-in-the-middle (MitM) attacks." The same applies to Node's rejectUnauthorized: false and Go's tls.Config{InsecureSkipVerify: true}. If a provider uses a private CA, add that CA to the trust store (REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS, a RootCAs pool) rather than turning verification off; /fix/tls-certificate-validation/ shows the fix for each common stack. ASVS 5.0.0 and OWASP API10 both require all API traffic over TLS.
A provider you have a contract with is still outside your trust boundary. Its systems, its other customers and anyone who compromises it can shape the data you receive and the connections it makes back to you. Design so that a bad provider is contained: validate and bound everything it sends, give it only the narrowest credential and scope it needs, and don't let its data drive privileged actions without your own checks.
Keep an inventory of the third-party APIs you call, the credential each uses and the data classification involved, which also answers API9:2023 Improper Inventory Management. When a provider is breached, that inventory tells you which integrations and credentials to rotate.
- One distinct, least-privilege credential per integration, stored in a secrets manager and rotated on a schedule (see /domains/identity-crypto/secrets/leaked-secret-response/).
- Egress controls so a provider URL you call can't be pointed at internal addresses; this is the SSRF overlap, covered in /domains/appsec/secure-features/ssrf-egress-boundaries/.
- Circuit breakers and bulkheads so a failing provider sheds load instead of taking the whole service down.
- Alerting on upstream error rates and latency, which is often the first sign a provider has changed or been compromised.
Retries smooth over transient failures but amplify load if they are naive. Retry only idempotent operations or operations you have made idempotent with an idempotency key, retry only on connection errors and 429 or 5xx responses, use exponential backoff with jitter, and cap the attempts. Honor a Retry-After header when the provider sends one. A retry storm against a struggling provider makes the outage worse for everyone, so pair retries with a circuit breaker. The queue and backpressure guide at /domains/runtime/availability/bounded-background-work/ covers running these calls off the request path.
Consumption runs both ways: providers also call you, through webhooks and callbacks. Treat an inbound webhook as untrusted until proven otherwise. Verify the signature with the provider's shared secret or public key over the raw body, check a timestamp inside the signed payload against a short window to stop replay, and for high-value events fetch the object back from the provider's API by ID rather than trusting the body. The webhook guide at /domains/appsec/secure-features/verify-webhook-deliveries/ has signature-verification code; don't reinvent it here.
Log enough to diagnose a flaky integration (the provider, endpoint, status, latency and a correlation id) but never the Authorization header, the API key, or a response body that may carry personal data. Redact credentials from any request dump, and make sure a client's debug logging is off in production. Monitoring these logs for error-rate and latency changes is often how you first notice a provider problem, which is why the inventory and alerting in the trust-boundary section matter.
| Case | What happened | Lesson |
|---|---|---|
| CVE-2018-18074, Python requests before 2.20.0 | The Authorization header was sent to an http URL on a same-host https-to-http redirect, exposing credentials to the network | Keep clients patched; don't assume a redirect keeps the same scheme or strips auth |
| CVE-2023-32681, Python requests before 2.31.0 | The Proxy-Authorization header was forwarded to the destination server on a redirect to HTTPS | Review which headers survive a redirect; scope proxy credentials |
| CVE-2025-21620, Deno before 2.1.2 | fetch kept the Authorization header when a response redirected to a different domain, against the Fetch standard | Handle redirects yourself for authenticated calls, or allowlist redirect hosts |
Guided practice
You are reviewing an orders service that enriches each order with data from three external APIs. The outbound code and notes below come from the review. Decide what is unsafe about each call and write the fix.
Supplied scenario material
C1 Python: data = requests.get(f"https://partner.example/addr/{zip}").json()
C2 Python: shipping = requests.get(url, verify=False, timeout=5).json()
C3 Node: const r = await fetch(providerUrl, { headers: { authorization: key } });
const body = await r.json(); // providerUrl comes from a stored integration record
C4 Go: resp, _ := http.Get(partnerURL) // http.DefaultClient
C5 The address field from C1 is concatenated into: INSERT INTO addr VALUES ('" + a + "')
N1 partner.example occasionally returns 200 with a 40 MB JSON blob.
N2 The shipping provider rotated to a new certificate last month; verify=False was added then.
N3 providerUrl values are editable by support staff in an admin screen.1. Fix the enrichment call and its sink
List what is wrong with C1 given N1 and C5, and write the safe version.
Expected result and reasoning
C1 has no timeout (requests never times out by default), no size cap (N1's 40 MB blob is read into memory), no schema validation, and its output is concatenated into SQL (C5), which is OWASP API10's stored-injection example. Fix: set
timeout=(3.05, 10), stream with a byte cap, validate the response with a strict schema, and use a parameterizedINSERT.2. Restore TLS verification
Explain why C2 and N2 are dangerous and give the correct fix.
Expected result and reasoning
verify=Falseaccepts any certificate and ignores hostname mismatches, exposing the integration to man-in-the-middle. A rotated certificate is not a reason to disable verification. Fix: trust the provider's CA viaREQUESTS_CA_BUNDLEor passverify="/path/to/ca.pem", and removeverify=False.3. Lock down the redirect and the URL source
Explain the risk in C3 given N3 and write the safe pattern.
Expected result and reasoning
fetch follows redirects by default, so a compromised or misconfigured provider can 302 the authenticated request (with the key) to another host; and
providerUrlis attacker-influenceable via the admin screen (N3), which is also an SSRF risk. Fix:redirect: "manual", allowlist the host, re-check each hop, and validateproviderUrlagainst an allowlist before calling.4. Give the Go call a timeout
Explain the problem with C4 and write the replacement.
Expected result and reasoning
http.Getuseshttp.DefaultClient, whoseTimeoutis zero, so a slow partner hangs the goroutine indefinitely. Fix: use a configured&http.Client{Timeout: 10 * time.Second}withTransportdial and TLS-handshake timeouts, and check the returned error instead of discarding it.5. Set the trust boundary
Describe the credential and inventory controls the service needs for its three providers.
Expected result and reasoning
One distinct least-privilege credential per provider from a secrets manager, rotated on a schedule; an inventory recording each provider, its credential and the data classification; egress controls so provider URLs can't reach internal hosts; and alerting on upstream error and latency spikes so a provider compromise is noticed.
The exercise reviews snippets on paper. It does not run the service, confirm the providers' real behavior, or prove the schema matches every valid response; a real review also needs contract tests against each provider and a check of the client library versions in use.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- OWASP API Security Top 10 2023: API10:2023 Unsafe Consumption of APIs
- Requests advanced usage: Timeouts and SSL cert verification. Requests 2.34.2.
- HTTPX: timeout configuration and defaults
- Pydantic: strict mode and model validation
- WHATWG Fetch Standard: HTTP-redirect fetch
- MDN: AbortSignal.timeout()
- Go net/http: Client and CheckRedirect
- NVD: CVE-2018-18074, requests sends Authorization to http URL on redirect
- NVD: CVE-2023-32681, requests leaks Proxy-Authorization on redirect
- GitHub Security Advisory GHSA-f27p-cmv8-xhm6: Deno fetch keeps Authorization across domains (CVE-2025-21620)
- OWASP ASVS 5.0.0, V13 Configuration. Version 5.0.0.
- OWASP Web Service Security Cheat Sheet
Reference information
- Tags
- Server-side request forgery (SSRF)Input validation and parsingDenial of service and resource limitsTLSAPI securityThreat modeling
- SDLC phase
- DesignDevelopment
- ASVS 5.0.0 requirements
- v5.0.0-1.3.6 · v5.0.0-13.1.1 · v5.0.0-5.4.1
- CAPEC attack patterns