Every appsec program tells developers to treat user input as hostile. Far fewer say the same about the JSON coming back from a partner’s API, a payment processor’s webhook, or an internal microservice three hops away. That asymmetry is the entire premise of Unsafe Consumption of APIs, ranked API10 in the OWASP API Security Top 10. The vulnerability isn’t a single bug pattern — it’s a trust decision made once, early, and never revisited: “this data came from a known company, so it’s safe to use without checking.”
It isn’t. If your service calls another API, that API’s compromise, misconfiguration, or malicious response becomes your incident.
How the Trust Gap Gets Exploited
Three patterns account for most real-world cases:
1. Following untrusted redirects and URLs. If a third-party API returns a URL — a webhook callback, an avatar link, a “next page” pointer — and your code fetches it without restriction, an attacker who controls or compromises that upstream service can redirect your server to 169.254.169.254 (cloud metadata) or an internal admin panel. This is SSRF, but the entry point is a trusted integration instead of a user-facing form field.
2. Deserializing responses without validating shape or type. APIs change, get compromised, or get proxied by an attacker in a supply-chain or DNS-hijack scenario. Code that assumes response.data.role is always a string and always safe to write straight into a database query or template will happily process a malicious payload that a real user could never submit directly.
3. Skipping TLS and schema enforcement because “it’s a known vendor.” Disabling certificate validation to work around a vendor’s expired cert, or accepting any HTTP status as success, removes the guarantees that make the integration trustworthy in the first place.
A 2025 incident involving a crypto staking platform illustrates the blast radius: attackers compromised a partner API used for a Solana staking integration and used the trusted channel to authorize unauthorized withdrawals worth roughly $41 million — the consuming application had no independent verification layer on top of what the partner API told it.
Fix: Treat External API Responses Like User Input
The mitigation is symmetric with input validation: schema-validate, restrict destinations, cap size, and never let a response make an authorization decision unilaterally.
JavaScript (Node.js) — validate response shape before use:
import { z } from "zod";
const PartnerPayoutSchema = z.object({
userId: z.string().uuid(),
amountCents: z.number().int().nonnegative().max(1_000_000),
status: z.enum(["pending", "settled", "failed"]),
});
async function fetchPayoutStatus(payoutId) {
const res = await fetch(`https://partner.example.com/payouts/${payoutId}`, {
signal: AbortSignal.timeout(5000),
});
if (!res.ok) throw new Error(`Partner API error: ${res.status}`);
const raw = await res.json();
// Never trust the shape or values just because the vendor sent them.
const payout = PartnerPayoutSchema.parse(raw);
return payout; // safe to use downstream
}
Python — validate with pydantic and refuse to follow arbitrary redirects:
import requests
from pydantic import BaseModel, PositiveInt, ValidationError
class Payout(BaseModel):
user_id: str
amount_cents: PositiveInt
status: str
def fetch_payout_status(payout_id: str) -> Payout:
resp = requests.get(
f"https://partner.example.com/payouts/{payout_id}",
timeout=5,
allow_redirects=False, # inspect redirects manually instead of auto-following
)
if resp.status_code != 200:
raise RuntimeError(f"Partner API returned {resp.status_code}")
try:
return Payout.model_validate(resp.json())
except ValidationError as e:
raise RuntimeError(f"Unexpected partner API response shape: {e}")
Go — bound response size and enforce an allowlist for any URLs the API hands back:
var allowedHosts = map[string]bool{"partner.example.com": true}
func fetchAndValidate(url string) ([]byte, error) {
parsed, err := neturl.Parse(url)
if err != nil || !allowedHosts[parsed.Host] {
return nil, fmt.Errorf("refusing to fetch untrusted host: %s", url)
}
client := &http.Client{
Timeout: 5 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return fmt.Errorf("redirects disallowed for third-party fetch")
},
}
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Cap read size to prevent a malicious/compromised upstream from exhausting memory.
limited := io.LimitReader(resp.Body, 1<<20) // 1 MB
return io.ReadAll(limited)
}
Checklist for Every Integration
- Enforce a strict response schema (zod, pydantic, JSON Schema) — reject anything that doesn’t match, don’t coerce it.
- Never auto-follow redirects from a third-party response without validating the destination host against an allowlist.
- Set explicit timeouts and response size limits on every outbound call.
- Keep TLS verification on, always — a broken vendor cert is a vendor problem, not a reason to disable validation.
- Give third-party-sourced data the same output-encoding and parameterized-query treatment as user input before it touches a database, shell, or template.
- Log and alert on schema-validation failures from partner APIs; a sudden shift in response shape is often the first signal of an upstream compromise.
Unsafe consumption of APIs persists because the fix looks identical to input validation — which makes it easy to assume it’s already covered. It isn’t, unless you’re applying it to every external response, not just the ones that arrive through a browser.
Sources: OWASP API10:2023 – Unsafe Consumption of APIs, Palo Alto Networks – What Is Unsafe Consumption of APIs?, APIsec – Unsafe API Consumption: Securing Third-Party Integrations