Skip to content

Cybersecurity Engineering Handbook / Chapter 13

Secure API and Service Design

Design APIs and services with explicit authentication, authorization, validation, output control, abuse resistance, logging, and operational ownership.

Chapter 12 left the support operator at a precise authorization decision: this person, using this console, may refund this order for this tenant up to this amount. Now the console sends POST /orders/ord_8041/refunds.

That short request creates new ways to get the decision wrong. A client can change the order identifier, repeat a timed-out request, add a field the server did not expect, ask for a larger response than the workflow needs, or issue requests faster than the payment system can safely absorb. A proxy can retry after the first refund has succeeded. Another service can call the handler without going through the console. None of these failures is repaired by the fact that the operator authenticated correctly.

An API is where a system turns an untrusted request into a controlled effect. Every endpoint therefore needs a declared caller, credential, authorization rule, input contract, output shape, work limit, evidence trail, and owner.

Side-by-side API request lifecycle. The secure API path moves through client, gateway, authentication, authorization, validation, handler, minimal response, and log. The BOLA attempt path shows an attacker changing an object id, authentication passing, object authorization failing, denial, and security log.
A secure API request is a sequence of gates. Authentication alone is not enough; object authorization, validation, minimal output, and logging must all survive direct calls.

Name the boundary before designing the route

“API” covers boundaries with different callers and consequences. A public API must tolerate unknown networks, hostile automation, and customer-visible error behavior. A partner API adds a commercial relationship, but still needs a distinct partner identity, scoped data agreement, quota, and revocation path. An internal API authenticates workloads and limits their authority; the word internal changes the likely attackers, not the need for a boundary. An admin API carries unusually powerful actions and needs named human attribution, step-up or approval where warranted, and a durable audit trail.

Event-driven APIs move the boundary to producers, brokers, schemas, and consumers. A command must name who is authorized to request an effect; an event must name which producer is trusted to attest that a fact occurred. GraphQL moves much of the endpoint surface into a schema and its resolvers, so field authorization, query cost, batching, and introspection policy become part of the boundary. A webhook is an API call delivered by somebody else’s retry schedule and therefore needs sender authentication, freshness, and duplicate handling.

Classify each exposed route, operation, event, and webhook. If one handler serves several classes, state which controls vary and which can never be weakened. The refund route is an admin API called by the support console, then an internal command consumed by the payment worker. Both boundaries must preserve the same order, tenant, amount, and approved intent.

Make the route express one effect

POST /orders/{order_id}/refunds names the parent resource and the effect being created. Its request body can remain narrow:

{
  "amount_minor": 4200,
  "currency": "USD",
  "case_id": "case_417",
  "reason_code": "duplicate_charge"
}

Stable nouns and honest method semantics help reviewers find where data is read and state is changed. They do not provide authorization by themselves. Nor does an opaque identifier. The service must resolve ord_8041 within the authenticated tenant and authorize the refund against the object it found.

Design list routes with the same care. Cap page size and total work; constrain filters, sort keys, cursors, and searchable fields; and apply tenant and discovery rules while forming the query. A page boundary must not become a way to cross an authorization boundary. Treat an export as its own high-volume action rather than an unusually large page.

Bulk operations amplify ambiguity. Authorize the collection-level action and every target, impose an item and work limit, and define whether one denial rejects the batch or produces explicit per-item results. For changes to money, identity, access, data sharing, keys, configuration, or production state, a small handler is not a small security decision.

Carry identity and policy through every gate

The gateway rejects malformed credentials early, but the refund service makes the decision that protects the effect. It validates the token’s signature, issuer, audience, time bounds, and intended use, then derives the human actor, calling workload, tenant, and authentication assurance from trusted claims. It does not accept tenant or role authority from the request body.

The handler resolves the order and asks the Chapter 12 policy question using the trusted actor, action, object, scope, and context. Required scope is only one input. A token bearing refund:write does not prove that the operator may refund this tenant’s order, that the amount is within their limit, or that the linked case is open. Field-level rules also govern which order attributes the operator may read and which refund fields they may set.

Keep the enforcement close to the protected operation and reuse it across the ordinary route, support tools, bulk handlers, message consumers, and scheduled jobs. Gateway-only and UI-only checks leave alternate entry paths exposed. When policy data, token keys, or decision services are unavailable, the endpoint needs an intentional failure behavior; an unknown decision must not quietly become an allow.

Turn input into one unambiguous request

Parse and validate before business work begins. For the refund, the contract defines required fields, types, allowed currency and reason values, integer bounds, maximum body and header sizes, identifier syntax, nesting depth, and unknown-field behavior. Rejecting unknown fields is useful when compatibility permits it: otherwise a misspelled field can disappear silently, and a field added to the server later can acquire unintended mass-assignment behavior.

Syntactic validity is not semantic validity. The service must still establish that the currency matches the order, the amount is positive and refundable, the case names this order, and the order remains in a state from which the transition is legal. Validate URLs, paths, hostnames, Unicode, case, and other values in the canonical form used by policy and storage. Avoid a sequence in which one layer authorizes one representation while another acts on a different one.

Apply bounds at every layer that allocates or performs work: request bodies, files, arrays, decompressed data, headers, query depth, resolver fan-out, database results, and downstream calls. Schema validation without work limits can still admit a perfectly shaped denial-of-service request. Parameterized database operations, context-appropriate encoding, restricted outbound destinations, and safe interpreters remain necessary after schema validation; the schema alone does not prevent injection or server-side request forgery.

Make retries repeat the answer, not the effect

The console sends an idempotency key for the logical refund attempt. The service binds that key to the authenticated principal, route, order, tenant, and a digest of the validated request. It records the result atomically with the accepted operation. A retry with the same key and same request receives the original outcome; the same key with a different amount is rejected.

The retention period must cover credible client, proxy, queue, and operator retry windows. Concurrency matters: two requests arriving together cannot both observe “not seen” and create refunds. An idempotency key limits accidental repetition; it is neither authorization nor a universal replay defense. An attacker who can create unlimited fresh keys still needs quotas and anomaly detection, while a signed message may also need a timestamp or nonce to limit replay after capture.

HTTP retries, message delivery, and downstream payment calls form one chain. Propagate the operation identifier, make the command consumer idempotent, and reconcile uncertain provider outcomes before attempting the financial effect again. A timeout means the caller lacks an answer; it does not prove that nothing happened.

Return a deliberate representation

A successful response can name the refund, its state, amount, currency, and a correlation identifier. It need not serialize the order, customer, support case, operator record, internal risk signals, or provider response. Returning an internal model makes every field added later part of an accidental public contract and possible disclosure.

Apply field authorization while constructing the representation, including GraphQL resolver results and nested objects. Give clients stable, useful error categories without returning stack traces, policy internals, query text, secrets, or evidence that helps enumerate other tenants’ objects. A correlation identifier connects a safe response to restricted operational detail.

Set cache policy from the sensitivity and caller model. Personalized or privileged responses normally require private or no-store; shared caching needs an explicit key that cannot mix identities or tenants. CORS controls which browser origins may read a response. Allow only intended origins, methods, headers, and credential behavior, but do not treat CORS as API authentication: non-browser callers are not constrained by it.

Budget abuse by the harmed resource

Authentication answers who presented a credential. Abuse control asks how much work or harm that actor may cause. For refunds, useful limits may apply per operator, tenant, order, payment instrument, support case, and time window—not merely per source IP. Separate a short burst allowance from a sustained quota, and define what happens when a limit store is degraded.

Cost is not always proportional to request count. GraphQL and flexible query APIs need limits on depth, aliases, list sizes, resolver fan-out, and estimated or observed cost. Search and export need result and compute budgets. Login and recovery need controls for credential stuffing and account enumeration. Public content may need scraping detection; sensitive mutations may need recent authentication, proof of intent, queue limits, or manual review.

Build abuse cases from assets and effects rather than from a generic list:

  • Change the object: submit another tenant’s valid order identifier and expect denial without disclosure or mutation.
  • Multiply the work: request the largest valid page, batch, nested query, or file repeatedly and expect bounded resource use.
  • Repeat the effect: race identical refunds, vary idempotency keys, and replay delayed commands; expect one authorized effect.
  • Change the path: attempt the same action through admin, bulk, GraphQL, event, and background-job paths; expect the same policy question.
  • Shape the response: request nested or newly added sensitive fields and inspect errors and caches for cross-tenant data.
  • Lose a dependency: make policy, quota, schema, or downstream services slow or unavailable and verify the chosen failure mode.

Each case needs an owner, expected prevention or detection, observable signal, and safe load-test method. Rate limiting that nobody can observe or tune is only a hidden failure mode.

Treat webhooks as hostile retries with a claimed sender

Suppose the payment provider reports refund.completed by webhook. Verify the signature over the exact bytes and signed metadata required by the provider’s protocol before parsing or transforming the body. Use the designated verification algorithm and constant-time comparison supplied by a maintained library. Accept the current and previous verification secret only for a bounded rotation interval.

Enforce a small clock tolerance or nonce rule, validate the event identifier, type, account, schema, and expected resource relationship, then process the event idempotently. A valid signature establishes that the holder of the verification secret produced those bytes; it does not authorize every event type to mutate every local object. Fetch authoritative provider state when the risk or protocol requires confirmation.

Do not assume delivery order, exactly-once delivery, or that an HTTP timeout means the sender will stop. Record duplicate, stale, invalid-signature, unsupported, and successfully applied events without logging the secret or unnecessary payload data. Return success only according to the provider’s retry contract.

Outbound webhooks need controls too: restrict destination schemes and ports, resolve and connect under an egress policy that blocks internal and metadata networks, protect signing secrets, bound redirects and response sizes, and rate-limit retries. Otherwise a customer-supplied callback URL can become a server-side request forgery primitive.

Let the contract expose the security decision

An OpenAPI document should describe the request and response schemas, security scheme, required scopes, status codes, content types, size expectations, and operation identifier. Standard fields cannot express the whole control. Use namespaced extensions such as x-authorization-rule, x-data-classification, x-idempotency, x-rate-limit-policy, and x-audit-event when they help generators and reviewers find repository-owned policy. Define each extension’s schema and semantics centrally; an undocumented x-secure: true is decoration.

Keep enforcement tests beside the contract. Test missing, malformed, expired, wrong-issuer, and wrong-audience credentials; valid credentials with wrong tenant, object, field, or scope; unknown and oversized input; duplicate and racing idempotency keys; quota exhaustion; unsafe cache behavior; and every documented error response. For GraphQL, test authorization in resolvers or the equivalent field execution layer, plus depth, cost, batching, aliases, and introspection behavior. Introspection may be needed by approved tooling; make its production exposure a conscious policy rather than assuming that hiding the schema substitutes for authorization.

Version the behavior that clients rely on, but do not preserve a dangerous gap indefinitely for compatibility. Record supported versions, control differences, compatibility tests, migration guidance, owner, notification and sunset dates, traffic remaining on the old version, and an emergency disable path. Security fixes sometimes require a breaking migration; the deprecation plan makes that change operable.

Operate the boundary

For the refund, one correlation chain should connect the gateway request, credential result, authorization decision, schema result, idempotency record, command, payment outcome, webhook, and final state. Log identifiers and reason codes rather than credentials, raw tokens, signatures, secrets, or whole sensitive bodies. Distinguish authentication failure, authorization denial, invalid input, throttling, duplicate delivery, dependency failure, and business rejection; collapsing them into request failed destroys the signal operators need.

Alert on patterns with security meaning: bursts across many accounts or objects, repeated cross-tenant denials, abnormal query cost, validation failure spikes, unusual admin actions, webhook signature failures, quota exhaustion, and use of versions near or beyond sunset. Baselines and thresholds belong to an owner who can investigate them. Retention and access controls for API logs should follow the sensitivity of the evidence they contain.

Before release, trace one endpoint from caller to effect and back:

  1. Name its class, caller, credential, object, tenant, and owner.
  2. Inspect its method, schema, bounds, canonicalization, and unknown-field behavior.
  3. Force authentication, scope, object, field, and tenant denials.
  4. Race and replay it; exhaust its work budget; fail its dependencies.
  5. Inspect the minimal response, errors, cache rules, CORS policy, logs, and alerts.
  6. Follow alternate routes—bulk, GraphQL, messages, jobs, and webhooks—to the same effect.
  7. Confirm its contract, tests, version policy, runbook, and deprecation owner agree with production behavior.

The refund endpoint is ready when a changed identifier cannot redirect authority, a repeated request cannot repeat the effect, a valid shape cannot create unbounded work, and every stop leaves evidence that explains which gate held. The webhook signature and token verification keys used along this path then become Chapter 14’s problem: cryptographic material with owners, rotation, revocation, and failure behavior.