Loading…
Loading…
Describes the Cross-Site Request Forgery (CSRF) protection for the admin API, which requires the X-Requested-With header for state-changing requests.
The Admin API CSRF Protection is a security filter that defends the platform's administrative endpoints against Cross-Site Request Forgery (CSRF) attacks. Administrative actions are typically authenticated using an ambient session credential, such as a browser cookie, which is automatically included in requests to the API origin. Without CSRF protection, a malicious website could trick an authenticated user's browser into sending unauthorized, state-changing requests to the admin API.
This filter mitigates this risk by requiring that all state-changing requests (e.g., POST, PUT, DELETE) to endpoints under the /admin/ path include a specific HTTP header that a browser cannot add to a cross-origin request without explicit permission (a CORS preflight). This proves that the request was initiated by same-origin client-side script, not a malicious third-party site.
The filter operates early in the request processing pipeline, before authorization checks occur, to reject forged requests as quickly as possible.
The CSRF filter intercepts all incoming HTTP requests and evaluates them to determine if they require CSRF validation. If a request is rejected, it is terminated with an HTTP 403 Forbidden response.
flowchart TD
A[Inbound HTTP Request] --> B{Is method state-changing?};
B -- No (GET, HEAD, etc.) --> G[Pass to next filter];
B -- Yes (POST, PUT, etc.) --> C{Is path under /admin/?};
C -- No --> G;
C -- Yes --> D{Has `X-Requested-With` header?};
D -- Yes --> G;
D -- No --> E{Has `Authorization: Bearer` header?};
E -- Yes --> G;
E -- No --> F{Has `Sec-Fetch-Site: same-origin` header?};
F -- Yes --> H[Pass to next filter (logs INFO)];
F -- No --> I[Reject with HTTP 403];A request is considered in-scope for CSRF protection if it meets both of the following criteria:
State-Changing Method: The HTTP method is not one of the safe, read-only methods. The set of safe methods is GET, HEAD, OPTIONS, and TRACE. Any other method (e.g., POST, PUT, PATCH, DELETE) is considered state-changing.
Admin Path: The request path targets an endpoint under /admin/. The filter uses a robust path matching algorithm to prevent evasion through URL encoding or path traversal syntax. It canonicalizes the path by decoding percent-encoded characters, stripping matrix parameters (e.g., ;foo=bar), collapsing repeated slashes, and resolving . or .. segments. If a path is undecodable, it is treated as in-scope to ensure it is checked (fail-closed).
If a request is in-scope, the filter checks for one of three proofs that it was not initiated by a cross-site forgery. If any one of these is present, the request is allowed to proceed.
X-Requested-With Header: The primary proof is the presence of a non-blank X-Requested-With HTTP header. This is a non-CORS-safelisted header, meaning a browser will not attach it to a cross-origin request without a successful CORS preflight. Since the admin API does not approve cross-origin preflights, the presence of this header proves the request originated from same-origin script. The platform's admin UI includes this header with the value s2r-admin-ui, but any non-blank value is accepted.
Authorization: Bearer Header: Requests that include an Authorization header with the Bearer scheme are exempt from the X-Requested-With requirement. Bearer tokens are not attached ambiently by browsers and must be added by client code. This provides the same level of assurance as X-Requested-With and allows machine-to-machine clients to call the admin API without modification.
IMPORTANT
Only the Bearer scheme provides this exemption. Other authentication schemes (e.g., Basic, Digest) are not exempt because a browser may automatically replay those credentials for any request to the origin, making them vulnerable to CSRF.
Sec-Fetch-Site: same-origin Header: As a fallback, the filter accepts the Sec-Fetch-Site header with a value of same-origin. This is a modern browser-controlled header that cannot be set by client-side script. It reliably indicates the relationship between the initiator's origin and the target's origin. This fallback ensures that older admin UI versions (which may not send X-Requested-With) continue to function with newer API versions, while still blocking cross-site and same-site requests. When a request is allowed solely based on this header, a message is logged at the INFO level to make the version skew observable.
Certain API paths are explicitly not protected by this filter because they have their own security mechanisms or are designed to be called cross-site:
/saml/acs: The SAML Assertion Consumer Service endpoint is designed to receive HTTP-POST requests from external Identity Providers and is secured by XML signature validation./internal/v1/specaria/**: Internal service-to-service APIs, which are secured by HMAC signature headers./health/**: Health check endpoints, which do not expose state-changing methods.If an in-scope request fails CSRF validation, the API responds with an HTTP 403 Forbidden status code and the following JSON error payload. This indicates the request was missing all required proofs of origin.
{
"error": {
"code": "S2R-ADM-0406",
"message": "State-changing admin requests must carry the X-Requested-With header (cross-site request refused).",
"details": {
"requiredHeader": "X-Requested-With"
}
}
}
When this error occurs, the system also generates a WARN log entry containing the request's path, method, and Origin, Referer, and Sec-Fetch-Site headers to aid in diagnostics.
A POST request to an admin endpoint without the X-Requested-With header will be rejected.
Failing Request:
curl -X POST "https://<api-hostname>/admin/v1/settings" \
-H "Content-Type: application/json" \
--cookie "session_id=<session-cookie>" \
-d '{"setting":"value"}'
# Expected Response: HTTP 403 Forbidden with error S2R-ADM-0406
Corrected Request:
To resolve the error, add the X-Requested-With header to the request.
curl -X POST "https://<api-hostname>/admin/v1/settings" \
-H "Content-Type: application/json" \
-H "X-Requested-With: my-api-client" \
--cookie "session_id=<session-cookie>" \
-d '{"setting":"value"}'
# Expected Response: HTTP 2xx (or other success/validation code)
The filter evaluates the following HTTP headers to make its decision.
| Header | Description |
|---|---|
X-Requested-With | Primary Proof. If present with a non-blank value, the request is allowed. The standard value sent by the admin UI is s2r-admin-ui. |
Authorization | Exemption. If the value starts with Bearer (case-insensitive) followed by a non-blank token, the request is allowed. |
Sec-Fetch-Site | Fallback Proof. If the value is same-origin (case-insensitive, trimmed), the request is allowed. Other values (cross-site, same-site, none) are rejected. |