In short

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.Client has 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 Authorization header to an http URL (CVE-2018-18074) and the Proxy-Authorization header across hosts (CVE-2023-32681); Deno's fetch kept Authorization across domains (CVE-2025-21620).
  • A partner API is outside your trust boundary even under contract; its compromise should not become your incident.

Verify it

  1. Grep the code for every outbound HTTP call and confirm each sets a timeout; flag requests.get/post without timeout=, Go clients using http.DefaultClient or a zero Timeout, and any fetch without an AbortSignal.
  2. 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.
  3. 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.
  4. Search for verify=False, rejectUnauthorized: false and InsecureSkipVerify and remove every one that is not a clearly scoped, documented test fixture.
  5. Check which headers your client sends after a cross-host redirect, so a provider cannot harvest your Authorization or Proxy-Authorization header (the failure mode behind CVE-2018-18074, CVE-2023-32681 and CVE-2025-21620).
  6. 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

Fix it in your stack

Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.

Learning goals and prerequisitesAbout 37 minutes: 10 to read, 5 on the code and 22 for the exercise

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 practice

Treat upstream data like user input

OWASP 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.

Treat upstream data like user input
ControlWhat goes wrong without itWhere it belongs
Response schema validationInjection and type confusion from upstream dataRight after the response is received
TimeoutsA slow or dead provider hangs your workersEvery outbound client
Size capA huge response exhausts memoryWhile streaming the body
Redirect policyRequest and credentials sent to an attacker hostThe client's redirect handling
TLS verificationMan-in-the-middle of the integrationThe client's TLS settings
Credential scopeA provider compromise reaches more than it shouldSecret issuance and rotation

Validate the response against a strict schema

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.

Fixedquotes.pyPython
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)

Set timeouts, because the defaults often don't

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.

Set timeouts, because the defaults often don't
ClientDefault timeoutHow to set one
Python requests 2.34None (waits forever)timeout=(3.05, 27) for connect and read
Python httpx 0.285 secondshttpx.Timeout(5.0, connect=3.0); never None in production
Node.js fetchNo total-request timeoutsignal: AbortSignal.timeout(5000)
Go net/httpNone (zero Timeout)http.Client{Timeout: 10 * time.Second} plus Transport dial/TLS timeouts
Java java.net.httpNo request timeout by defaultHttpRequest.newBuilder().timeout(Duration.ofSeconds(10))

Don't follow redirects blindly

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.

A fixed fetch wrapper: timeout, manual redirects, size cap

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.

Fixedclient.mjsJavaScript
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"));
}

Validate the parsed response in TypeScript

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.

Fixedquote.tsTypeScript
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
}

A fixed httpx client in Python

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.

Fixedupstream.pyPython
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))

Test the timeout and redirect behavior

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.

Testtest_upstream.pyPython
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()

Keep TLS verification on

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.

Draw the trust boundary around each provider

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.

Retry carefully, and make writes idempotent

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.

Verify inbound callbacks from providers

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 upstream failures without leaking secrets

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.

Learn from client libraries that leaked across redirects

Learn from client libraries that leaked across redirects
CaseWhat happenedLesson
CVE-2018-18074, Python requests before 2.20.0The Authorization header was sent to an http URL on a same-host https-to-http redirect, exposing credentials to the networkKeep clients patched; don't assume a redirect keeps the same scheme or strips auth
CVE-2023-32681, Python requests before 2.31.0The Proxy-Authorization header was forwarded to the destination server on a redirect to HTTPSReview which headers survive a redirect; scope proxy credentials
CVE-2025-21620, Deno before 2.1.2fetch kept the Authorization header when a response redirected to a different domain, against the Fetch standardHandle 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
Plain text
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. 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 parameterized INSERT.

  2. 2. Restore TLS verification

    Explain why C2 and N2 are dangerous and give the correct fix.

    Expected result and reasoning

    verify=False accepts 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 via REQUESTS_CA_BUNDLE or pass verify="/path/to/ca.pem", and remove verify=False.

  3. 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 providerUrl is attacker-influenceable via the admin screen (N3), which is also an SSRF risk. Fix: redirect: "manual", allowlist the host, re-check each hop, and validate providerUrl against an allowlist before calling.

  4. 4. Give the Go call a timeout

    Explain the problem with C4 and write the replacement.

    Expected result and reasoning

    http.Get uses http.DefaultClient, whose Timeout is zero, so a slow partner hangs the goroutine indefinitely. Fix: use a configured &http.Client{Timeout: 10 * time.Second} with Transport dial and TLS-handshake timeouts, and check the returned error instead of discarding it.

  5. 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.

Your deliverable: A fixed version of C1-C4 with timeouts, size caps, redirect and TLS policy and schema validation, a parameterized version of C5, and a short note on the credential scoping and inventory for the three providers.

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.

  1. OWASP API Security Top 10 2023: API10:2023 Unsafe Consumption of APIs
  2. Requests advanced usage: Timeouts and SSL cert verification. Requests 2.34.2.
  3. HTTPX: timeout configuration and defaults
  4. Pydantic: strict mode and model validation
  5. WHATWG Fetch Standard: HTTP-redirect fetch
  6. MDN: AbortSignal.timeout()
  7. Go net/http: Client and CheckRedirect
  8. NVD: CVE-2018-18074, requests sends Authorization to http URL on redirect
  9. NVD: CVE-2023-32681, requests leaks Proxy-Authorization on redirect
  10. GitHub Security Advisory GHSA-f27p-cmv8-xhm6: Deno fetch keeps Authorization across domains (CVE-2025-21620)
  11. OWASP ASVS 5.0.0, V13 Configuration. Version 5.0.0.
  12. OWASP Web Service Security Cheat Sheet

Reference information

ASVS 5.0.0 requirements
v5.0.0-1.3.6 · v5.0.0-13.1.1 · v5.0.0-5.4.1

Download this guide as Markdown