Every request to an API is inspected like any other request, JSON and XML bodies included: an injection in a JSON field is found the same way as one in a form. API Protection (under Detection) holds what is particular to APIs, set per site: the methods an API uses and limits on its routes on every licence, and - with PortWall Enterprise - the API's specification, cross-origin rules, sign-in tokens and GraphQL limits.
Methods
Browsers use GET, HEAD, POST and OPTIONS, and PortWall refuses any other method - which a REST API's PUT, PATCH and DELETE also are. For a site whose API uses them, tick them under Also allow. Nothing else changes for that site, and other sites keep refusing them.
Route limits
The box-wide rate limit on Global WAF Defaults counts every request from an address alike, so it has to be generous enough for a busy visitor. Some routes deserve a limit of their own: a login, a password reset, a token endpoint, a search. A route limit counts only the requests to that path and answers 429 Too Many Requests once an address goes over it, before the request is inspected.
- Route - a path ending in
/covers everything under it (/search/). One that does not covers itself and everything under it:/api/logincovers/api/loginand/api/login/..., but not/api/loginx. - Requests per 10 seconds, minute or hour.
- Counted per address, or per TLS fingerprint - which catches a client that keeps changing its address. Counting per fingerprint needs TLS fingerprinting on Bot Protection.
Each site's limits are its own: the same route on two sites is counted separately, with each site's own limit. A site can have up to 20 route limits.
Cross-origin requests (CORS)
Enterprise. Before a page on another site calls your API from its script, the browser asks your site whether that origin may (a preflight), and it lets the page read the answer only if your site names its origin. Many sites get this wrong - answering every origin, or none - and a site under the browser check cannot answer a preflight at all.
- Off - your site answers these questions itself, as before.
- Report only - a page's script call that would change something (anything but GET and HEAD) from an origin you have not listed is reported in the Attack Feed. Nothing else changes. Start here.
- Answer and enforce - PortWall answers the preflights of the origins you list, replaces your site's own cross-origin headers with its answer, and refuses (403) a page's script call that would change something from any other origin.
Allowed origins - one per line, like https://app.example.com, with no path. https://*.example.com covers every subdomain of example.com (not example.com itself). null is a sandboxed page, and is allowed only if listed. * alone means any origin - for a public API - and cannot be combined with credentials.
Your site's own hostnames are always allowed. A page navigating to your site - a payment provider or a bank sending the visitor back to your shop with a form post - is not a cross-origin call and is never refused.
Only these paths limits all of this to your API (/api/); left empty it covers the whole site. Headers a page may send are Authorization and Content-Type unless you list others, and the methods offered are GET, HEAD, POST and the ones the site allows under Also allow. Credentials lets a page send the visitor's cookies and read the answer.
A preflight is never sent to the browser check, whatever the check covers.
API specification
Enterprise. Upload the API's OpenAPI document and PortWall checks every request to the API against it - what an API gateway checks, without changing the API.
- The document - OpenAPI 3.0 or 3.1, JSON or YAML, in one file (references to other files are not followed: bundle them first), up to 1 MB and 500 operations. A document PortWall cannot use is refused with the place that is wrong, such as
paths./v1/orders.post.requestBody: must be a mapping. Uploading a new one replaces the old one when you save. - Base path - where the API lives on the site, taken from the document's first server URL (
https://api.example.com/v1gives/v1). Change it when the API is served somewhere else;/is the whole site. Requests outside it are not checked. Paths are compared without regard to case, as IIS serves them. - What is checked, in this order: the path and the method are in the specification; the required parameters (path, query, header, cookie) are there and of the right type and range; a body's content type is one the operation declares; a JSON body matches the operation's schema.
- Report only puts every request that breaks the specification in the Attack Feed - for example API: POST /v1/orders: body.quantity must be an integer - and lets it through. Refuse answers it 400 Bad Request. Start in Report only: a specification often differs from the API it describes, and the feed shows where before anything is refused.
- Endpoints not in the specification (API: GET /v1/export is not in the specification) are reported in both modes. They are refused (403) only when Refuse endpoints not in the specification is ticked as well: a specification that leaves out one live endpoint would otherwise break it.
- A body over the inspection limit (on Global WAF Defaults) is not checked against its schema, and is reported as too large to check.
- What is left out of a document is listed under it: a pattern that uses look-ahead or back-references, and a path parameter the document does not describe. A field marked
readOnlyis not required of a request.
The methods and content types the specification uses are offered, never added on their own. Allow them ticks PUT, PATCH or DELETE under Also allow (then save). A content type that the rule set refuses by default - anything but JSON, XML, forms and file uploads - is named; if those requests are refused, exclude rule 920420 for the API's path in the site's tuning.
The check follows the site's WAF mode, so a site in detection-only reports instead of refusing, and without a licence nothing is refused. Without Enterprise the check is not applied; its settings and the document are kept, and apply again when the licence includes Enterprise.
Sign-in tokens (JWT)
Enterprise. An API that signs its users in with tokens from a sign-in provider (Microsoft Entra ID, Auth0, Okta, Keycloak, Google, your own) can have PortWall check the token before a request reaches the site. A request to the route without a valid token is refused 401 Unauthorized - before its body is read - so a forged or expired token, or a caller without one, never reaches your code. The site keeps its own check: PortWall does not pass the token's contents on, and leaves the Authorization header as it was.
Add a route with Add a sign-in token route and set:
- Route - as for route limits:
/api/covers everything under it,/api/orderscovers itself and everything under it. Upper and lower case, and tricks like/public/../api/, are the same route. Leave every method unticked to cover all of them, or tick the ones that need a token. - Off / Report only / Refuse - a new route starts in Report only: every request without a valid token shows in the Attack Feed with the reason (Token refused: expired 3 minutes ago, no token, wrong audience, claim scope does not include orders:write) and goes through. Switch to Refuse once the feed shows only what you expect - a wrong audience refuses every call. Refused requests count no points towards blocking an address, so a client with an old token never blocks its whole network.
- Keys - where the token's signature is checked against:
- The issuer's OpenID configuration (most providers): enter the issuer exactly as it appears in the tokens (
iss), for examplehttps://login.example.com/. PortWall reads/.well-known/openid-configurationunder it to find the keys. - A key set address (JWKS): for a provider without that, give the address of its key set. The issuer, if entered, must still match
iss. - A shared secret (HS256, HS384 or HS512): for an application that signs its own tokens with a secret. See below.
- The issuer's OpenID configuration (most providers): enter the issuer exactly as it appears in the tokens (
- Algorithms - RS256 and ES256 by default; PS256-512, RS384/512, ES384/512 and EdDSA can be added.
noneis never accepted. - Audiences - the token's
audmust name one of them (leave empty not to check). - Required claims - up to five. Equals one of: the claim is one of the values (
roleequalsadmin, clerk). Includes: the claim - a list, or a space-separated string likescope- includes every value listed (scopeincludesorders:write). - Clock skew - how far the token's expiry and not-before times may be off this server's clock: 60 seconds, up to 300. A token without an expiry is always refused.
- Token in -
Authorization: Bearer(the usual place), a header of your own, or a cookie.
Keys are kept current. Keys are fetched when you save - a save whose key set does not answer is refused, so a mistyped issuer shows at once - and then every 15 minutes, or as the provider's own caching says (between 5 minutes and a day), only over HTTPS. A route is checked from the moment it has keys. If the provider cannot be reached, PortWall keeps using the keys it had - it never stops checking - and the route's status says so: Key set unreachable since 09:12 UTC - using the 3 keys from before. The status turns amber after a day and red after a week; a provider that has rotated its keys by then will see its new tokens refused.
Shared secrets. Choose a shared secret only when the application has no public keys. Enter it under Secret and press Set secret: it is at least 32 bytes for HS256, 48 for HS384 and 64 for HS512, taken as text (write base64: in front of one generated as bytes). It is never shown again (leave the field empty to keep it), never logged, kept apart from the other settings and not in backups - after a restore, enter it again; until then the route is not checked and says so. Bear in mind that with a shared secret this server can also sign tokens your API accepts, and so can anyone who can read the secret here. A shared-secret route accepts only its one algorithm, so a public key can never be passed off as the secret.
Browser check. API clients cannot answer the browser check (Bot Protection). If a token route's callers are programs rather than browsers, add the route to the browser check's exempt paths - the route's card reminds you when it is not.
GraphQL limits
Enterprise.
A GraphQL API answers whatever one request asks for, and to the rest of the inspection that request is one piece of text: a single request can ask for data nested fifty levels deep, the same expensive field a thousand times under different names, or a hundred operations at once. GraphQL limits read each request to the site's GraphQL endpoint the way the site's GraphQL server will, and stop the ones that ask for too much.
Tick GraphQL limits for the site. It starts in Report only: requests over a limit are shown in the Attack Feed - "GraphQL: depth 23 is over the limit of 12" - and still reach the site. Once the feed shows only what you expect, choose Refuse requests over a limit, and those requests are answered 400 Bad Request.
- Endpoint paths - where the site's GraphQL API answers,
/graphqlif you leave it empty. The path is matched exactly, ignoring letter case and a trailing slash; list each one if the site has several. - Largest query - 32 KB. Checked before the query is read; the queries of a batch count together.
- Deepest nesting - 12.
{ me { friends { name } } }is 3. - Most aliases - 30. An alias asks for a field under another name, which is how one request asks for the same thing many times.
- Most fields - 1,000, counted with every fragment expanded: a fragment used in ten places counts ten times. A query whose fragments refer back to themselves is refused whatever the limits.
- Most operations in one request - 10. A batch sends several operations in one request.
Requests sent as JSON (one operation or a batch), as application/graphql, as a GET with a query parameter and as a file upload are all checked. A persisted query - sent as its hash, without the query text - is passed, because the site's server runs only queries it already knows; it still counts towards Most operations. A query that is not valid GraphQL is refused too.
The schema query (introspection) lets anyone read the API's whole schema: every type, field and argument, the map an attacker uses to find what to try. It is refused by default, as the OWASP guidance advises and most GraphQL servers do in production. Developer tools such as GraphiQL, Postman and code generators ask for it, so list the addresses they work from under Except from these addresses - addresses or ranges such as 198.51.100.0/24, one per line. Untick Refuse the schema query only for an API that is meant to be public.
The limits still apply to an address that may read the schema. The schema query's own unwrapping of list and required types (ofType) is not counted towards the depth, so the standard schema query these tools send fits the default limits.
Backups
API protection is part of every backup made on Backup & Restore.