CSRF, CORS, and SSRF: Defending Web Request Boundaries
Learn how CSRF, CORS, and SSRF differ, then apply cookie, origin, allowlist, and egress controls to protect browser and server request boundaries.
CSRF, CORS, and SSRF each govern a different boundary: browser actions made with cookies, browser access to cross-origin responses, and server requests to external destinations. The guide shows how to pair CSRF tokens and origin checks, scope CORS to known origins, and keep SSRF validation tied to the address actually reached. TypeScript examples and a URL-validation sketch make the controls concrete, while failure scenarios and review checklists help teams catch gaps before deployment.
CSRF, CORS, and SSRF: Defending Web Request Boundaries
Introduction
A request can cross a security boundary in several ways: a browser may send a user’s cookies after another site triggers a form, browser code may try to read a response from a different origin, or a server may fetch a user-supplied URL and reach an internal service.
These risks need different defenses. CSRF controls protect authenticated browser actions, CORS governs which origins browsers may read, and SSRF defenses constrain outbound requests. This article explains where those controls apply and how to avoid common gaps such as treating CORS as authorization or relying on a simple hostname check.
What each attack looks like
CSRF: a browser reuses ambient credentials
Browsers attach matching cookies automatically. If a user is signed in to bank.example, a malicious page might submit a request to that site. The attacker’s page cannot usually read the response, but reading is not required if the action itself transfers money or changes an email address.
A CSRF attack relies on the browser sending credentials the attacker cannot see or set directly. SameSite cookie attributes reduce exposure in many cases, but they are not a complete policy for every browser flow, subdomain setup, or older client. Pair an appropriate cookie policy with a server-side CSRF token or strict origin checks for state-changing requests.
CORS: a browser decides whether JavaScript may read a response
A server sends CORS response headers to tell a browser whether a page at another origin may access a response. For requests that need a preflight, the browser may first send an OPTIONS request. A failed CORS check blocks browser script access; it does not undo a request that the browser already sent, and it does not stop non-browser clients.
For credentialed requests, browsers require an explicit allowed origin and Access-Control-Allow-Credentials: true. A wildcard origin cannot be combined with credentialed access. Reflecting whatever Origin header arrived, without checking it against a deliberate allowlist, gives hostile sites permission to read responses.
A focused guide to Spring configuration is available in Spring Boot CORS configuration.
SSRF: the server becomes a confused deputy
With server-side request forgery, an attacker convinces your application to make a request on their behalf. If the server can reach private network addresses, a URL preview feature might be abused to probe internal services or cloud metadata endpoints. DNS rebinding and redirects can bypass simplistic checks that inspect only the original hostname.
Request boundary map
Browser CSRF/CORS Flow
graph TD
A[Browser request] --> B[Cookie and origin checks]
B --> C[Application authorization]
C --> D[State-changing handler]
A --> E[CORS response policy]
Server-Side SSRF Flow
graph TD
F[User-supplied URL] --> G[URL and destination validation]
G --> H[Restricted outbound network]
H --> I[Remote service]
A practical implementation shape
Check the origin and CSRF token on unsafe browser requests
Use a framework’s CSRF middleware when available; it knows how that framework parses forms, sessions, and tokens. The following TypeScript sketch shows the decision order. sessionStore and token generation are application-specific, and the token must be bound to the authenticated session and compared in constant time.
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
const TRUSTED_ORIGINS = new Set(["https://app.example.com"]);
async function requireBrowserRequestProtection(
req: Request,
): Promise<Response | null> {
if (SAFE_METHODS.has(req.method)) return null;
const origin = req.headers.get("origin");
if (!origin || !TRUSTED_ORIGINS.has(origin)) {
return new Response("Forbidden", { status: 403 });
}
const session = await sessionStore.get(req);
const submittedToken = req.headers.get("x-csrf-token");
if (
!session ||
!submittedToken ||
!verifyCsrfToken(session, submittedToken)
) {
return new Response("Forbidden", { status: 403 });
}
return null;
}
Do not put a CSRF token in a cookie that JavaScript cannot distinguish from the session cookie and assume that alone solves the problem. A synchronizer token is returned to the legitimate page through a protected response and sent back in a header or form field. Keep state changes off GET; crawlers, previews, and browser prefetching may issue safe-method requests without user intent.
Return CORS headers for a known set of frontend origins
Allow only the methods and headers the frontend uses. Return the matching origin only after checking it against configuration, and add Vary: Origin if the response varies by origin so shared caches do not reuse the wrong policy. Keep CORS policy separate from authentication and authorization.
const FRONTEND_ORIGINS = new Set([
"https://app.example.com",
"https://admin.example.com",
]);
function corsHeaders(origin: string | null): Headers {
const headers = new Headers({ Vary: "Origin" });
if (origin && FRONTEND_ORIGINS.has(origin)) {
headers.set("Access-Control-Allow-Origin", origin);
headers.set("Access-Control-Allow-Credentials", "true");
headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
headers.set(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-CSRF-Token",
);
}
return headers;
}
Handle preflight requests before authentication middleware that expects a user session; an OPTIONS preflight normally does not carry the application credentials. The actual request still needs authentication, authorization, input validation, and CSRF protection where applicable. For related identity checks, see API authentication vs. authorization.
Constrain outbound URL fetches
The safest SSRF policy is a narrow one: accept only required schemes, reject credentials embedded in URLs, resolve names and reject private or reserved address ranges, connect only to a validated address, and disable redirects or revalidate every redirect target. The HTTP client and DNS stack must preserve the validated destination through connection setup; otherwise a hostname can resolve differently between validation and connect.
fetchExternalResource(inputUrl):
parsed = parse(inputUrl)
require parsed.scheme is https
require parsed.hostname is in the feature's allowed host set
require parsed has no username or password
addresses = resolve(parsed.hostname)
require every selected address is globally routable
connect to a selected validated address while preserving TLS hostname checks
do not follow redirects automatically
if a redirect is required, repeat the full validation for its new target
cap response size, time, and redirect count
An allowlist of exact hosts is often easier to reason about than a list of blocked IP ranges. If arbitrary public URLs are a product requirement, enforce network egress rules too. Application checks reduce mistakes; a network policy that prevents access to metadata services and private ranges limits damage when a check fails. Store credentials in a dedicated secret system and never forward internal authorization headers to a fetched destination. See secrets management practices for credential handling.
Production failure scenarios and mitigations
| Failure | Why it happens | Mitigation |
|---|---|---|
| A state change succeeds from an attacker-controlled page | Cookie credentials are sent automatically and the handler accepts the request without an intent check | Use session-bound CSRF tokens, validate Origin on unsafe methods, and set an appropriate SameSite cookie policy |
| A site can read private API responses | CORS reflects arbitrary origins or a broad trusted-origin rule includes attacker-controlled subdomains | Use an exact origin allowlist, review subdomain ownership, and test credentialed responses from an untrusted origin |
| URL validation passes, but the request reaches an internal IP | DNS changes after validation, a redirect points inward, or the HTTP client resolves independently | Bind validation to the actual connection, disable or revalidate redirects, and restrict outbound network access |
| Security checks break legitimate traffic after deployment | The allowlist omits a real frontend origin, proxy origin headers differ, or preflight gets rejected by authentication middleware | Log policy denials without secrets, stage origin changes, and exercise preflight and real requests through the production proxy path |
| A URL fetcher consumes excessive resources | The remote endpoint streams a huge response or stalls | Set connection and total timeouts, response-size limits, and concurrency limits |
Observability without leaking credentials
Record the control that rejected a request, its route, the normalized origin or destination category, and a request ID. For SSRF denials, record whether the rejection came from scheme, host, address, redirect, timeout, or size policy. This helps distinguish an attack from a broken integration.
Do not log cookies, CSRF tokens, authorization headers, full URLs that may contain user information, or response bodies by default. Use counters for allowed and rejected CORS origins, CSRF validation failures, and outbound destination denials. Alert on a sudden rise, but keep low-volume security events available for investigation. Scrub sensitive fields before logs leave the application; the same care applies to API input validation and sensitive data.
Security review points
Before shipping a browser-facing feature, confirm that every state-changing route rejects unexpected origins and requires the appropriate CSRF token when it uses ambient cookies. Check that GET requests do not mutate state. Review cookie Secure, HttpOnly, and SameSite settings alongside session rotation and XSS defenses; a stolen session through XSS is a separate problem that CSRF tokens do not fix.
For CORS, list the exact frontend origins, methods, and headers required. Do not treat a successful preflight as proof that the caller is authorized. For URL fetch features, decide whether arbitrary destinations are actually required, review DNS and redirect behavior in the chosen HTTP client, and enforce egress restrictions around metadata endpoints and private services. Keep service credentials out of outbound requests unless that destination explicitly needs them.
Trade-Off Table
| Control choice | Benefit | Cost or limit |
|---|---|---|
| Session-bound CSRF token | Explicitly ties a state-changing request to the user’s session | Requires token delivery and validation on each unsafe browser request |
| Strict origin check | Simple signal for browser requests from an approved site | Must define behavior for missing or proxy-rewritten origins; it does not replace authorization |
SameSite cookie policy |
Reduces when browsers attach cookies to cross-site requests | Can affect legitimate navigation or embedding flows and is not a complete CSRF policy |
| Exact CORS origin allowlist | Limits which browser origins can read API responses | Requires maintaining origins across environments; CORS does not prevent non-browser callers |
| Fixed provider IDs for server fetches | Keeps destinations narrow and easier to validate | Less flexible when users need arbitrary public URLs |
| Arbitrary URLs with egress controls | Supports flexible fetch features while limiting network reach | Requires careful DNS, redirect, address, timeout, and response-size checks across app and network layers |
Common pitfalls
- Using CORS as the only CSRF defense. CORS controls response access in browsers; it does not reliably prevent the request from being sent.
- Allowing every subdomain with a suffix string check.
endsWith("example.com")can accept names such asnotexample.com; parse hostnames and compare exact values or carefully bounded subdomains. - Trusting
RefererorOriginwithout accounting for absent, malformed, or proxy-rewritten headers. Define a deliberate failure policy and test it in deployment. - Blocking only
127.0.0.1. Private IPv4 and IPv6 ranges, link-local addresses, DNS aliases, and redirects also matter. - Checking a hostname once, then letting a different resolver or automatic redirect choose the connection destination.
- Returning a friendly CORS header on an error path that exposes sensitive response details to a broader origin.
Quick Recap Checklist
- State-changing browser routes do not use
GET. - Cookie-authenticated unsafe requests validate a session-bound CSRF token and/or a strict origin policy.
- Session cookies use appropriate
Secure,HttpOnly, andSameSiteattributes. - CORS allows only required origins, methods, and headers; credentialed access uses explicit origins.
- Authentication and authorization run on the actual request, regardless of CORS outcome.
- User-influenced fetches restrict schemes and destinations, revalidate redirects, and bind checks to the connection.
- Outbound network rules block access to private infrastructure and metadata services.
- Time, response size, and concurrency limits bound URL fetch work.
- Logs capture policy outcomes and request IDs without tokens, cookies, or sensitive URL data.
Interview Questions
Further Reading
- Spring Boot CORS configuration — Apply an origin allowlist in a Spring application.
- API authentication vs. authorization — Keep identity checks separate from resource permissions.
- API input validation, secrets, and sensitive data — Validate untrusted inputs and protect sensitive fields.
- Secrets management — Store and rotate credentials used by services.
- OWASP CSRF Prevention Cheat Sheet — Review token, cookie, and origin-based defenses.
- MDN: CORS — See how browsers handle cross-origin response access and preflight requests.
- OWASP SSRF Prevention Cheat Sheet — Apply destination validation and network-layer controls.
Conclusion
Keep each check at the boundary where it matters: verify browser intent before a state change, set a narrow policy for cross-origin reads, and validate a destination before the server connects. Egress rules and logs help contain failures when an application check misses something.
Category
Related Posts
API Authentication vs. Authorization: Identity and Access
Understand API authentication and authorization, how they differ in request handling, and how to avoid common identity and access-control mistakes.
API Input Validation, Secrets, and Sensitive Data
Validate API inputs at trust boundaries, handle secrets safely, and limit sensitive data exposure in logs, storage, and error responses.
API Keys, Sessions, and Service Credentials Explained
Compare API keys, browser sessions, and service credentials, then choose storage, rotation, and transport practices that fit each API client.