An API security test is not a scan with a longer word list. It is a set of assertions about who may do what to which object, run against an inventory you can count. The defects that produce API breaches are rarely exotic: an endpoint that accepts an object identifier and returns the object without checking ownership, a token whose audience or signature was never validated, a collection endpoint that returns every tenant's rows, a six-digit one-time code with no per-account limit, and a state change with no transaction boundary. Every one of those is testable with two accounts and a script. What most engagements lack is not tooling but arithmetic: how many assertions were run, against how many endpoints, as how many roles, and what the endpoint inventory actually contained before the test began.
The test you run is only as good as the inventory behind it
Start with the number nobody has. Akamai's 2026 API Security Impact Study, a survey of 1,840 security leaders and practitioners across ten countries, reports that 87 per cent of respondents experienced at least one API-related security incident in the previous twelve months, up from 76 per cent in 2022. The same study found that 77 per cent claim a full API inventory, but only 23 per cent of those know which of their APIs return sensitive data - a figure that has fallen for four consecutive years rather than risen. Discovery studies point the same way: Cloudflare's machine-learning endpoint discovery reported roughly 31 per cent more API endpoints than organisations had catalogued themselves.
That gap is the first finding of any API test, and it is measurable before a single request is sent. If you test only what the OpenAPI document lists, you test the part of the surface that somebody remembered to document, which is normally the part that was built most carefully.

Remote Monitoring Offers
Choose Sphnix first, then compare mSpy and Eyezy.
Four inventories, four different truths
- The specification. OpenAPI, gRPC protobuf, or the GraphQL SDL. Finds documented endpoints and their declared security schemes. Misses everything shipped without a spec update, and mis-states anything where the spec drifted.
- Observed traffic. Thirty days of gateway or edge logs, grouped by method and route template. Finds the shadow endpoints real clients call, including mobile builds that were never retired.
- Client artefacts. Strings and route tables inside the mobile app bundles and the web JavaScript. Finds endpoints only the app calls: internal admin paths, legacy versions, debug routes left switched on.
- Hostnames and deployments. Certificate transparency logs, DNS records, and every preview or staging environment the CI system ever created. Finds the environment with production data and no rate limit.
The rows that are almost always missing
- The previous major version, still live for old mobile clients, with the older authentication model and no rate limiting.
- Collection and export endpoints: the list call that returns many objects, and the CSV or bulk export that returns all of them.
- Service-to-service routes reachable from the same ingress as the public API, authenticated by a shared header rather than a token.
- Partner and webhook receiver endpoints, which accept data from third parties and are the least reviewed code on the estate.
- The preview deployment of last week's branch, still resolving, pointed at a copy of the production database.
Reconcile the four lists into one table with a row per endpoint and method: route, authentication scheme, whether an object identifier appears in the path, query or body, the data classification it returns, whether it is publicly reachable, and the owning team. That table is the deliverable the test is measured against. It is also the only honest way to answer the question a client will ask afterwards: was my API tested, or was a sample of it tested?
Two practical notes. First, an inventory produced for a test is stale within a release cycle unless the pipeline regenerates it - the useful version is generated from the spec plus the observed traffic on a schedule, not maintained by hand. Second, the inventory is where the test's cost lives. An endpoint count gives you a defensible estimate; an estimate without one is a number somebody invented to win the work.
Authorization is the finding that matters, and no scanner finds it
Broken object level authorization has held the top position in the OWASP API Security Top 10 since the list was created, and for a structural reason: the API authenticates who you are and then fails to check whether this object belongs to you. A scanner cannot find it, because finding it requires two valid identities and a valid object identifier for each - things a scanner does not have. Scanners hit a 401 or a 403 and move on; the vulnerable endpoint returns 200 only when the caller is a real user asking for a real object that is not theirs.
The test itself is old and boring, and it works. Create two accounts with the same role. As account A, create an object and capture its identifier from the response. As account B, request that identifier. A correct API returns 403, or 404 if it chooses not to disclose existence. A vulnerable one returns 200 with A's data. Everything else in this section is that test, repeated where people forget to repeat it.
The five variants of the same defect
- Read. B requests A's object by identifier in the path, the query string, the body, and a header - all four locations, because handlers are often inconsistent about which one they authorise.
- Write. B patches or puts to A's object. A handler that checks ownership on read and forgets it on update is common enough to test separately, not infer.
- Delete. Only against seeded objects. This is the variant that turns a test into an incident when it is run carelessly.
- Nested. B requests A's child object through a path that implies ownership via the parent: an invoice under an order, an attachment under an invoice, a comment under a post. Each hop is a chance to check the parent and forget the leaf.
- Collection. B calls the list endpoint with A's identifier as a filter parameter, or with no filter at all in a multi-tenant system. The single-object handler is usually correct; the list handler frequently is not, and it leaks at scale.
In a multi-tenant product, the tenant identifier must come from the validated token, never from the request. A test that only swaps object identifiers inside one tenant will pass while cross-tenant reads remain open, so the two accounts must sit in different tenants as well as different users. The related trap is the identifier format: random UUIDs slow enumeration down and do not replace the check, because an identifier exposed by any other endpoint, export, email or log is replayable like any other. OWASP makes the same point about comparing the subject claim in a token to an identifier in the request - it covers a small subset of cases and leaves the rest to a server-side authorisation decision made per object and per action.
What the fix looks like matters for the report. An authorisation check placed in one shared layer that every handler must pass through fixes a class of defects. A check added to the handler that failed fixes one endpoint and leaves the next release to rediscover the pattern. Say which one you are recommending; the difference is the reason a finding recurs in next year's test.
The coverage matrix: turning "test every endpoint" into a number
Every checklist says the same thing and stops there: test every object-scoped endpoint with two accounts. Nobody converts that sentence into work, which is why two engagements on the same API can differ by a factor of ten in what they actually exercised. Do the arithmetic instead.
Count the endpoints that accept an object reference - path, query, body or header. Call that E. Count the distinct identities you can test as: two ordinary users in different tenants, one user with a second role, one administrator, and the unauthenticated caller. Call that R. The authorisation surface is E multiplied by R, and each cell is two requests: the caller's own object must succeed, and a foreign object's identifier must fail. A 120-endpoint API where 70 endpoints take an object reference, tested across four identities, is 280 assertions and 560 requests.
Worked example: 120 endpoints, 4 identities
- 70 of the 120 endpoints take an object identifier, so E = 70.
- Identities: user A, user B in another tenant, an admin, and unauthenticated, so R = 4.
- Assertions = 70 x 4 = 280. Requests = 2 per assertion = 560.
- Scripted runtime at two requests per second per identity: under five minutes. The human effort is building the seed fixture per role, not sending the requests.
- If the report says "admin endpoints were reviewed manually", that row is outside the matrix and the coverage number must say so.
That number does three jobs. It sizes the work and therefore the price, it gives the client a denominator they can compare against another vendor's proposal, and it gives the retest a baseline: the same script, run again, produces 280 rows that either pass or fail. A vendor who cannot state a denominator should be asked for the script, because the script is the coverage.
Sampling is legitimate, and it should be deliberate rather than random. Three classes of endpoint deserve to be in every sample regardless of size: the newest ones, which have had the least review; the oldest ones, which have had the least maintenance; and the administrative and partner-facing ones, which have the fewest users and therefore the fewest bug reports. A random sample over a mature API mostly re-tests the endpoints whose bugs were already found by someone else.
The 403-or-404 decision leaks more than it protects
Most guidance treats the choice between 403 and 404 for a foreign object as a matter of taste. It is a product decision with a measurable consequence, and the test is cheap: take the identifier of an object that exists but belongs to A, and an identifier that does not exist at all, and request both as B. Compare status code, response body, error message, headers and timing.
If the existing object returns 403 and the missing object returns 404, the API has just confirmed existence to an unauthorised caller. With sequential identifiers, that turns a failed authorisation check into an enumeration oracle: an attacker learns the size of the object space, the rate of growth, and which identifiers are live, without ever being authorised for one. If the reasoning is that 403 is better UX for a legitimate user who bookmarked their own object from another account, the fix is to make the decision per route and document it, not per handler and inconsistent.
What the test asserts
- Same status code for foreign and non-existent identifiers.
- Same response body shape and length, without the identifier echoed back.
- No difference in the error text that names the real owner, the object type, or the field that failed.
- Timing difference under a few milliseconds; a database lookup that happens before the authorisation check shows up as a measurable gap.
- The same behaviour on the update and delete methods, not only on read.
Why it is worth its own finding
- It survives authorisation fixes that return 403 everywhere, so it is a defect a client can close only after it is named.
- It converts a read-only exposure into a mapping capability, and mapping is the first phase of an attack chain.
- It is one line in the framework's error handler once somebody decides the policy, which makes it an unusually cheap finding to close.
Token and session tests that do not need a payload
The authentication layer produces the second-largest share of API incidents, and its defects are testable with a text editor and a replay tool rather than an exploit. Wallarm's 2026 analysis of disclosed API breaches attributed 52 per cent of them to broken authentication and 27 per cent to unsafe consumption of third-party APIs, and noted that a majority of the API vulnerabilities it catalogued could be reached without authenticating at all. Treat those shares as a description of where the defects cluster, not a law of nature - the useful part is the list below.
Nine assertions on the token path
- Algorithm pinned. A token with the algorithm set to none, or re-signed with HMAC using the service's own public key as the secret, must be rejected. Both are old attacks that still land on hand-rolled verifiers.
- Signature actually checked. Flip one character in the signature and expect 401. A verifier that parses claims and skips the signature check is more common than the literature suggests.
- Key source fixed. The key identifier, JWKS URL and certificate URL fields must not cause the service to fetch a key from a location the caller controls.
- Expiry enforced server-side. An expired token must fail, and the clock skew tolerance must be small enough to state in the report. A 24-hour grace window is a finding.
- Audience and issuer validated. A token minted for one service must not be accepted by another. This is the defect that turns the weakest service in an estate into a key to the rest.
- Token type enforced. An identity token or a refresh token replayed as an access token must fail.
- Revocation window measured. Log the account out, then replay the access token. Many systems cannot revoke one for the token's lifetime; that is a design choice, and the report should state the window in minutes rather than call it a pass or a fail.
- Refresh rotation. Reusing a consumed refresh token must invalidate the whole family, otherwise a stolen token survives the victim's password change.
- Transport. Tokens in query strings end up in access logs, referrer headers, browser history and proxy caches. Check the logs before assuming they do not.
API keys deserve their own four questions, because they are the credential most often issued and least often governed: what scope does the key carry, when does it expire, who can rotate it, and where is it stored on the client? A key embedded in a mobile application is a public key, whatever the documentation calls it. A key with no expiry and no per-key rate limit is a permanent, unmetered credential, and the report should describe it that way.
Property-level authorization, or the field you never meant to accept
The same missing check that produces broken object level authorization produces mass assignment: the handler binds the request body onto a model, so every field the client sends becomes a field the database stores. The test is a single request. Take an endpoint that updates your own object, add one privileged field at a time, and watch the response.
The three request classes for every write endpoint
- Unknown field. A field that does not exist in the schema. A 200 means the handler ignores unknown keys, which is good; a 500 means the binding layer is unguarded.
- Known but not yours. A field that exists and that your role should not set: role, tenant identifier, verification flag, credit balance, price, approval status, timestamps. A 200 with the value applied is the finding, and it is worth more than a stack trace.
- Read-side over-fetching. The same defect in reverse. Compare the keys in the response against the keys the client actually uses; a user object that returns a password hash, an internal note, a soft-deleted flag or another tenant's name is a data exposure even when the UI never shows it.
The durable fix is two allowlists per endpoint - writable fields by role, readable fields by role - kept next to the route definition rather than generated from the model. Framework-level "fillable" maps drift the moment somebody adds a column, and the drift is invisible until a test enumerates it. On the read side, a projection defined per endpoint also solves the export problem: an export that serialises the model returns everything the model has, which is why export endpoints show up in the same finding as the list endpoint.
Business logic: test the arithmetic, not the payload
Checklist-driven testing is enumeration-shaped: it looks for identifiers to swap and parameters to fuzz. The defects that cost money are arithmetic-shaped, and they need the tester to know what must remain true. Write down the invariants first; then each one is a test.
Six invariants cover most commerce and workflow APIs. Money is conserved: the sum of line items equals the total, and refunds never exceed payments. Quantities are non-negative and integral where the product requires it. A discount is used once, cannot be stacked when the rules say it cannot, and does not apply to a category excluded by the promotion. The state machine only moves forward and only through allowed edges: nothing ships before payment, nothing is refunded twice, a cancelled order cannot be reopened by replaying an old request. Quotas and seats are enforced on the server, not in the client that hides the button. Time windows are enforced at the moment of the action: a trial that expired yesterday cannot be used today by holding a token issued during the trial.
Two examples show why this class never appears on a scanner report. A refund endpoint that accepts a negative amount turns a refund into a charge, or the reverse, depending on which side of the sign the validation was written. A quantity of zero that voids an order while still decrementing stock creates a permanent mismatch between the ledger and the warehouse - no error, no alert, just a number that stops reconciling three weeks later. Neither looks like an injection; both are found by a tester who knows the arithmetic the business expects.
Idempotency belongs here too. Send the same payment or redemption request twice with the same idempotency key and expect one effect; send it twice without a key and expect the API to reject the duplicate rather than execute it. A client that retries on timeout is normal behaviour, and an API without idempotency turns normal behaviour into a duplicate charge.
Rate limits, keyspace arithmetic and the endpoints that must have one
"Implement rate limiting" is the least actionable sentence in most API guidance. Limits exist to bound two different things - guessable secrets and expensive work - and the endpoints that need them are not the same. Do the arithmetic for the first class rather than arguing about request per second numbers in the abstract.
What a keyspace gives an attacker
- A six-digit one-time code is 1,000,000 possibilities. At 10 attempts per second with no lockout, an expected-value exhaustion takes about 27.8 hours; at 1,000 attempts per second it takes about 17 minutes.
- A four-digit PIN is 10,000 possibilities: about 17 minutes at 10 attempts per second, seconds at a higher rate.
- A password reset token built from six characters of a small alphabet is in the same class as the OTP, and is usually issued with a 15-minute validity that only helps if the attempt limit exists.
- The conclusion is structural: on credential endpoints the limit must be per account and must count failures, with a lockout or backoff after a small number. A per-IP limit alone does not bound the attack on a single account, and it is the wrong key for another reason below.
Per-IP keying fails for a reason that has nothing to do with attackers. Mobile carriers place thousands of subscribers behind one carrier-grade NAT address, and corporate networks do the same through a proxy. A limit that binds the attack also blocks a meaningful fraction of legitimate users at peak, so teams raise it until it binds nothing. The keys that work are the identity in the token, the account being acted on, and the API key; IP is a supplementary signal for unauthenticated endpoints, not the primary control.
Test five things on every credential and every expensive endpoint. That a limit exists for the identity. That a second, stricter limit exists for the account on login, one-time code, password reset and invitation acceptance. That the limit counts failures rather than successes. That exceeding it returns 429 with a retry hint rather than a 500 that a retry storm amplifies. And that expensive endpoints - search, report generation, image conversion, bulk export - are limited by cost rather than by request, because a single request against a 400-column export is not comparable to a lookup.
Two limits get forgotten because they are not rate limits at all. Pagination caps: a page size parameter that accepts a million returns the whole table, and once authorization is weak anywhere the export becomes the exfiltration path. Structural limits: maximum body size, maximum JSON depth, maximum array length. An endpoint that accepts a batch of identifiers is an endpoint that accepts ten thousand identifiers, and each one is a lookup the caller did not have to authenticate for individually.
GraphQL, webhooks and file upload: the surfaces the checklist forgets
GraphQL changes the shape of the test rather than the logic. Introspection is usually disabled in production, which stops nobody: field suggestions in error messages ("did you mean") still enumerate the schema one guess at a time, and a client bundle usually contains the operations the product actually uses. The test is the same authorization test, applied at the wrong layer if you are not careful - the object-level check must live in the resolver or the data layer, because a parent field that checks tenant membership does not protect the nested selection underneath it.
Batching is where GraphQL defeats rate limiting in a way worth naming explicitly. A single HTTP request can carry hundreds of aliased operations, each executed as a separate resolver call. A limiter that counts HTTP requests sees one; the database sees five hundred. The test is a request containing several hundred aliased login attempts or object lookups, and the result is either one rate-limit response or a batch of answers. The mitigation is cost-based limits computed from the parsed query, persisted queries or an allowlist of operations, and a maximum query depth - not a request counter in front of the endpoint.
Webhook receivers
- Signature verification over the raw request body, before JSON parsing. Re-serialising the parsed object changes the bytes and breaks honest verification, which is why so many receivers "verify" nothing.
- A replay window on the signed timestamp, measured in minutes, so a captured delivery cannot be replayed next week.
- Whether the payload can make the receiver fetch a URL - that is server-side request forgery with a delivery receipt.
- Whether an unsigned delivery is accepted when the signature header is simply absent. Deleting a header is the cheapest attack there is.
File upload
- Content type from magic bytes, not from the header the client sent.
- Where the file lands: a public bucket with predictable names is a data exposure even when the upload endpoint is authorised.
- Name handling: traversal sequences, and filenames that become object keys.
- Whether an uploaded SVG or HTML is later served from the application's own origin, which turns an upload into stored cross-site scripting.
Race conditions and the two-request test
A check-then-act sequence is correct in sequence and wrong in parallel, which is why it survives code review, unit tests and a full security review that reads the handler top to bottom. The test is two identical requests sent at the same moment against a state-changing endpoint: redeeming a gift card, withdrawing a balance, applying the last seat in a class, claiming a one-time pickup slot. One request should succeed and the other should be rejected; two successes mean the balance was spent twice.
The reason it belongs in an API test rather than a code review is that the defect is not visible in any single handler: it is the absence of a transaction boundary or a unique constraint. The fix is the same - a database constraint or a conditional update that the second request cannot satisfy, plus an idempotency key on the request - and not an application-level lock, which works on one instance and fails on the second. When a client asks why an in-process mutex is not enough, the answer is the deployment diagram.
Running destructive tests without damaging production
Most API tests are ordered by the checklist rather than by risk to the system under test, which is how a delete test lands on a real customer's record at four in the afternoon. Order the work by blast radius instead, and make the ordering part of the scope document.
Four bands, in this order
- Read-only. Enumeration, inventory reconciliation, token assertions, authorization on read and list endpoints. Safe on production with rate limiting you control.
- Non-destructive writes. Mass assignment, business logic and state transitions, executed only against objects the tester created in a dedicated test tenant with an identifiable marker.
- Destructive writes. Delete and cancel tests against seeded objects only, with cleanup run as a query against the marker rather than a manual hunt.
- Resource exhaustion and load. Only inside a written window, with the on-call engineer informed, a stop condition, and an explicit statement in the authorization that this class of test is permitted. Absent that sentence, it is not permitted.
The other discipline that is missing from most guides concerns the evidence itself. If a test proves access to a record containing personal data, capture the request, the status code, the count of affected records and the field names; hash one sample value so the finding is reproducible; then delete the body. Screenshotting a customer's invoice to prove a finding is how a security test becomes a data protection incident, and it is the first question a lawyer asks when an audit follows. Under the UK and EU GDPR the principle is the same one the tester should already apply: minimise, and keep only what proves the point.
What automation can and cannot prove
The honest split matters more than the tool list, because the most common failure in a mature programme is a client who believes the pipeline covers the API. Automated checks are genuinely good at four things: transport and header configuration, dependency versions against known advisories, specification conformance (a linter that catches endpoints declaring no security scheme at all, or parameters that shadow an authorisation header), and schema fuzzing that turns 500s into findings. None of those four is where the breaches are.
They cannot find object authorization, because the scanner has no valid identifiers and no second identity. They cannot find business logic, because the invariant lives in the business, not in the schema. They cannot find chained flows, where three individually harmless endpoints combine into privilege escalation. They cannot find a race condition, and they cannot find a rate-limit bypass that arrives as one legitimate-looking batched request.
What can be automated is narrower and more valuable than most teams expect: the authorization matrix itself. A script with two seeded accounts per role, run against staging on every deploy, asserting that a foreign identifier returns 403 or 404 and that the caller's own identifier returns 200. Every critical endpoint gets a row, the script fails the build on a cross-account 200, and the team stops re-introducing the same defect in new handlers. That is the one control that changes the trend line, and it is also the retest artefact that makes a manual finding stay closed.
The complementary piece is on the code side. An authorization check that exists in only some handlers is a code review problem, and a review that focuses on the diff rather than the pattern will keep approving it. Pairing an API test with a secure code review of the authorization middleware, the token verifier and the request-binding layer catches the class where a test can only catch instances.
Reporting: severity that reflects reachability, not category
A finding's severity should describe what an attacker gets, not which line of a top-ten list it resembles. Four axes do that better than any single score. Reachability: public internet, internal network, or pre-authentication. Authorisation required: none, any authenticated user, a privileged user. Data class: credentials, payment data, personal data, or telemetry. Blast radius: one object, one tenant, or the whole platform.
Run the same defect through those axes twice and the point becomes obvious. Broken object authorization on an internal endpoint behind network policy and a privileged role is a medium finding. Broken object authorization on a public endpoint that returns another customer's invoice is a reportable personal-data exposure that a script can repeat a thousand times a minute, and the report should say the rate out loud, because the rate is what turns a bug into an incident.
What makes a finding actionable
- The exact request, with the two identities and the two identifiers named.
- The fix location: gateway policy, handler, or data layer. A finding without one is re-litigated in the next meeting.
- The class it belongs to, and whether the recommended fix closes the class or the case.
- The assumption you tested under, stated explicitly, including anything you could not test.
What closes it
- The same request, against the same seeded fixture, now returning 403 or 404 - not a screenshot of a green dashboard.
- The assertion committed to the deploy pipeline, so the closure outlives the person who made it.
- A retest window written into the engagement: thirty days is typical, and it should cover the fix rather than the report.
- A note on residual risk where the fix is partial, because partial fixes are the normal outcome.
Scope and cost: why the day count is endpoints times roles
Published rate cards converge on the same shape because they are all pricing the same arithmetic. Budget Security's 2026 rate card puts an API test at four to five days for ten to twenty-five endpoints, six to nine days for forty to eighty with mixed roles, and ten to seventeen days for a hundred or more endpoints across several services. Published vendor proposals for a mid-sized API assessment cluster between roughly 3,000 and 28,000, with several firms quoting a floor around 14,500 for anything enterprise-shaped. None of those numbers mean anything until you know which endpoints and how many identities, which is why the coverage matrix is the first page of a proposal rather than an appendix.
Six variables move the estimate, and a client can usually influence all of them. The number of methods supported per endpoint, because a resource with create, read, update and delete is four tests and not one. The role model, because five roles multiply the matrix and a role hierarchy adds escalation tests on top. Whether the surface is documented, because discovery is days of work before the first assertion. Whether the client supplies seeded accounts and fixtures per role, because building them can be a fifth of the engagement on its own. Whether load testing is in scope, which changes the authorization document as well as the calendar. And the retest terms, because a fix that is not retested is a claim rather than a result.
What belongs in the scope document is therefore unglamorous and specific: the endpoint list with a count, the role list, the environments, who owns the seed fixtures, the test window, whether load or denial-of-service testing is permitted or explicitly excluded, the report format, the retest window, and a line stating that partner and vendor APIs consumed by the product are out of scope without their own written authorisation. A penetration test that covers web and mobile alongside the API is usually scheduled as one engagement, because the same session and the same test accounts serve both; the penetration testing guide covers the web-application side of that scope, and a web security audit is the right vehicle when the API sits behind a product that has never been reviewed at all.
Cheap ways to shrink the bill
- Send the OpenAPI document and the gateway logs before the kickoff, so discovery is a comparison rather than an archaeology project.
- Create the seeded accounts yourself, one per role per tenant, with a documented password. This is the single largest variable a client controls.
- Retire the previous API version before the test rather than during it. Every live version is a second surface with the same matrix.
- Fix the 403-or-404 policy and the authorization layer before the retest, so the retest closes rows instead of re-finding them.
The lawful boundary and the authorization document
Every request in an API test is an access request against somebody's system, and the only thing separating a test from an attack is a document signed before the first one is sent. That document names the systems and hostnames, the source addresses, the time window, the permitted techniques, the excluded techniques, the data handling rules, the emergency contact, and the person authorised to widen the scope in writing while the test is running. If a test needs to expand mid-engagement, the expansion happens on paper first; a verbal "go ahead" from an engineer is not authorisation for the company.
Three boundaries get blurred often enough to state plainly. Testing your own systems is unconstrained in permission and constrained by change control. Testing a client's systems is bounded by the contract, and the contract does not extend to their vendors. Testing an API your product consumes - a payment processor, a mapping provider, an identity service - is not authorised by your relationship with them as a customer, and their terms almost always forbid it; the valid alternative is to test how your integration handles malformed, delayed and hostile responses from a mock. And an API you discovered but do not own is never in scope, no matter how obviously broken it looks.
The last boundary is the one that decides whether the engagement ends as a security report or an incident: demonstrating impact is not the same as taking the data. Prove access with one record and minimal fields, record the count, stop. An API test that pulls a customer table "to show the scale" has crossed from authorised testing into unauthorised processing, and the client's lawyer will not read the scope document sympathetically at that point.
A day-one test plan and the fix order
The plan below is deliberately ordered so that the cheap, high-yield assertions happen before anything that touches data or load. It fits a single day on a mature API with seeded accounts, and it produces the finding that matters most in most engagements - a missing object-level check on a collection endpoint.
Day one, in order
- Diff the spec against observed traffic and log every endpoint in neither. Ask what they are before testing them.
- Build the identity set: two users in different tenants, one second role, one admin, unauthenticated.
- Run the nine token assertions and record the revocation window in minutes.
- Run the object authorization matrix on the five endpoints with the highest data classification, then on every collection endpoint.
- Compare foreign-versus-missing identifiers for status, body, timing and error text.
- Send three mass-assignment requests per write endpoint: unknown field, privileged field, read-side over-fetch.
- Measure the per-account limit on login, one-time code, password reset, and invitation acceptance.
- Run one two-request race against the highest-value state change.
- Test one upload and one webhook receiver.
- Write the coverage number while the evidence is fresh: assertions run divided by endpoints times identities.
The fix order is the same in almost every report, and it is worth saying so rather than ranking findings by CVSS alone. First, enforce object authorization in one shared layer that every handler passes through, because that closes a class instead of a case. Second, validate tokens server-side with the algorithm pinned and audience and issuer checked, because a token accepted by the wrong service is a lateral movement primitive. Third, put per-account limits with failure counting and lockout on every credential endpoint, because that is the only control that bounds the keyspace arithmetic. Fourth, define response projections so that models stop leaking fields nobody asked for. Fifth, add database constraints and idempotency keys to the state changes that move money or stock. Sixth, write the 403-or-404 policy down once and enforce it in the error handler.
None of that requires a rewrite, and all of it is testable afterwards by the same script that found the problem. That is the standard worth holding an API test to: a report whose findings can be re-run, whose coverage can be counted, and whose fixes leave an assertion behind in the pipeline. If you would rather have that run against your own API by people who do it for a living, tell us what the API does and we will scope it against an endpoint count instead of a hunch.
Sphnix Monitoring Dashboard
Track messages, location, social media and activity signals with an authorized monitoring dashboard.
Try Sphnix Now →Related Sphnix Features:
Need Professional Help?
Hire verified ethical hackers for authorized recovery, security testing and incident support.
Hire a Hacker →Professional Services
Explore cybersecurity services for audits, recovery, forensics and defensive hardening.
View Services →Questions? Our experts are ready to help.
Contact Us for Free Consultation →Frequently Asked Questions
Two cadences, not one. The authorization matrix - two seeded accounts per role asserting that a foreign object identifier returns 403 or 404 - should run on every deploy to staging, because its cost is minutes and its failure mode is a regression nobody notices. The full manual engagement should run at least annually and on any change to the authorization model, the tenant model, the authentication provider or a partner integration, because those changes invalidate the assumptions the last test was built on. Regulated environments tighten the annual floor: PCI DSS requires application penetration testing at least annually and after significant change, and ISO 27001 control A.8.8 expects technical vulnerability management to be continuous rather than annual.
No. Finding it requires two valid identities and a valid object identifier belonging to each, and a scanner has neither: it authenticates as at most one account, has no reason to hold a second tenant's objects, and treats a 403 as the end of the conversation rather than the beginning of a test. Scanners are useful for transport configuration, dependency versions, spec conformance and schema fuzzing, but the authorization defects that cause most API breaches are found by a script that logs in twice and swaps an identifier.
Five things: the specification (OpenAPI, protobuf or SDL) plus thirty days of gateway logs so the inventory can be reconciled; seeded accounts for each role in each tenant, with documented credentials; a dedicated test tenant or clearly marked seed data so destructive tests cannot touch real records; written authorization naming the hosts, the time window and the permitted techniques; and a named person who can approve scope changes in writing during the test. The seeded accounts matter most - building them is often a fifth of the engagement when the client cannot supply them.
Published rate cards put a ten to twenty-five endpoint API at four to five days of effort, forty to eighty endpoints with mixed roles at six to nine days, and a hundred or more endpoints across several services at ten to seventeen days; vendor proposals for a mid-sized assessment cluster between roughly 3,000 and 28,000, with several firms quoting a floor around 14,500. The driver is endpoints multiplied by identities, plus discovery effort if the surface is undocumented, plus whether the client supplies the seed fixtures. Ask any vendor for the coverage denominator and the day count follows from it.
Not on the strength of your customer relationship. A vendor's terms almost always forbid security testing of their service, and your authorization covers your systems, not theirs. Test your side instead: stand up a mock that returns malformed, delayed, duplicated and hostile responses and confirm your integration validates them, handles errors without leaking data, and fails closed. If you believe the vendor's API is insecure, report it through their security contact or disclosure programme rather than testing it.
