---
key: "third-party-api-consumption"
title: "Consume third-party APIs safely (OWASP API10:2023)"
url: "https://softwaresecurity.com/domains/appsec/secure-features/third-party-api-consumption/"
api: "https://softwaresecurity.com/api/v1/articles/third-party-api-consumption.json"
domain: "appsec"
topic: "Application security"
type: "Guide"
identifiers: ["CWE-20","CWE-295","CWE-770","CWE-601","CWE-522","A03:2025","A05:2025","API10:2023","API7:2023","V1.3.6","V13.1.1","V5.4.1","CAPEC-141","CAPEC-275"]
tags: ["Server-side request forgery (SSRF)","Input validation and parsing","Denial of service and resource limits","TLS","API security","Threat modeling"]
stages: ["design","build"]
roles: ["developer","appsec-engineer"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Consume third-party APIs safely (OWASP API10:2023)

Treat every upstream and third-party API response as untrusted input: validate it against a schema, set connect and read timeouts, cap the response size, refuse to follow redirects blindly, keep TLS verification on, and scope the credentials you hold for each provider.

Canonical URL: https://softwaresecurity.com/domains/appsec/secure-features/third-party-api-consumption/

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

## Recommended defaults

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

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

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

Audience: Developers who integrate payment, identity, enrichment or partner APIs, and reviewers checking outbound HTTP clients.

Before you start:

- Make HTTP requests from server code in at least one language.
- Know the difference between input you validate and data you trust.

Time: About 37 minutes: 10 to read, 5 on the code and 22 for the exercise

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

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

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

```python
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.

| 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))` |

## 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`.

```js
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.

```ts
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.

```python
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.

```python
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.

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

## 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



| 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 material

```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. Fix the enrichment call and its sink

List what is wrong with C1 given N1 and C5, and write the safe version.

Worked answer: 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. Restore TLS verification

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

Worked answer: `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. Lock down the redirect and the URL source

Explain the risk in C3 given N3 and write the safe pattern.

Worked answer: 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. Give the Go call a timeout

Explain the problem with C4 and write the replacement.

Worked answer: `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. Set the trust boundary

Describe the credential and inventory controls the service needs for its three providers.

Worked answer: 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.

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.

Scope: 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.

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [OWASP API Security Top 10 2023: API10:2023 Unsafe Consumption of APIs](https://api-security.owasp.org/editions/2023/en/0xaa-unsafe-consumption-of-apis/)
- [Requests advanced usage: Timeouts and SSL cert verification](https://requests.readthedocs.io/en/latest/user/advanced/#timeouts). Requests 2.34.2.
- [HTTPX: timeout configuration and defaults](https://www.python-httpx.org/advanced/timeouts/)
- [Pydantic: strict mode and model validation](https://pydantic.dev/docs/validation/latest/concepts/strict_mode/)
- [WHATWG Fetch Standard: HTTP-redirect fetch](https://fetch.spec.whatwg.org/#http-redirect-fetch)
- [MDN: AbortSignal.timeout()](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static)
- [Go net/http: Client and CheckRedirect](https://pkg.go.dev/net/http#Client)
- [NVD: CVE-2018-18074, requests sends Authorization to http URL on redirect](https://nvd.nist.gov/vuln/detail/cve-2018-18074)
- [NVD: CVE-2023-32681, requests leaks Proxy-Authorization on redirect](https://nvd.nist.gov/vuln/detail/cve-2023-32681)
- [GitHub Security Advisory GHSA-f27p-cmv8-xhm6: Deno fetch keeps Authorization across domains (CVE-2025-21620)](https://github.com/denoland/deno/security/advisories/GHSA-f27p-cmv8-xhm6)
- [OWASP ASVS 5.0.0, V13 Configuration](https://github.com/OWASP/ASVS/blob/v5.0.0_release/5.0/en/0x22-V13-Configuration.md). Version 5.0.0.
- [OWASP Web Service Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Web_Service_Security_Cheat_Sheet.html)