something I don't think I've ever seen explained is whether there's any situation where it's safe to set "Access-Control-Allow-Origin: *" other than "if your site literally never serves any private data"

(I often hear "don't do it" which is fair I guess, but also like the Mastodon API intentionally sets Access-Control-Allow-Origin: * and that's extremely useful)

also is there any name for the attack(s) that setting "Access-Control-Allow-Origin: *" might expose you to? i feel like it's so much easier to talk about security stuff in terms of the specific threats we're trying to avoid, but I can't think of the name for it

(edit: I think it's CSRF)

huh I'm not sure if this is true but this post argues that it's generally fine to set Access-Control-Allow-Origin: * (as long as you don't set Access-Control-Allow-Credentials, and as long as the API is public and not on an intranet) https://advancedweb.hu/is-access-control-allow-origin-star-insecure/
Is Access-Control-Allow-Origin: * insecure?

Disabling a security feature is usually a bad thing. In this case, it's fine

@b0rk i have found in the last like, ten years of internetting that convincing people to take shit offline and put it behind a firewall or nat or SOMETHING so that the entire planet cant see/hit it is a sorta sisyphean task. it hasnt stopped me from trying tho :D
@Viss @b0rk it's also common for admins of such sites to leave the default password in place (and most software/appliances have static or externally predictable default creds). Sisyphus had an easier task.
@b0rk Very very "it depends." Any resource that is account-sensitive or susceptible to CSRF as mentioned above should be locked down harder. On the other hand, in some cases you need ACAO: *.
@mttaggart interesting, thanks! been trying to find reasons why setting ACAO: * might be problematic (other than the 2 reasons in that blog posts) and i haven't been able to yet but I'll keep looking

@mttaggart @b0rk I think this is the other thing that's challenging about CORS (and security in general) -- it's hard to get a "yes, that's safe" answer. There's a mix of "it depends" and "here's _one_ example of an unsafe pattern", but "I know two unsafe patterns" does not indicate that all other patterns are safe.

In general, it's hard to prove a negative (i.e. "there are no security flaws in this design"), and since the security discussion is generally pointed towards risk-avoidance, you'll rarely get clear "do X" advice. If you can produce some on this (like a "web security basics" zine), I would buy it. 😁

@evana @mttaggart I'm very far from being a security expert but usually I think of security discussions in terms of threat modelling, and I feel like knowing about more possible risks helps me make my threat models more accurate

@b0rk What that page misses is consideration of the question: is there any chance the content on my site could be used maliciously?

That is, if my site allows user-uploaded content, how sure am I that there's zero chance my site might end up hosting something malicious?

Which, of course, has nothing to do with credentials.

Or, I guess, put another way: yes, credential and identity exfiltration is one vulnerability addressed by CORS (and CSP), but it's not the only one.

One specific example here is a scenario where a site allows users to upload their own avatar image, say in PNG format. But instead of sending a PNG, the user uploads a JS file but sends its content-type: image/png. The site thinks "it's just an image" and doesn't bother to check that the content is actually valid.

If that site then has ACAO: *, it has just become a hosting service for malicious code.

Write-up for an SVG alternative to this attack: https://www.cloudflare.com/cloudforce-one/research/svgs-the-hackers-canvas/

SVGs: the hacker’s canvas

Cloudforce One research reveals a surge in phishing attacks using SVG files, exploiting their scriptable nature to bypass detection. Discover how this overlooked vector poses a growing threat to email security.

@b0rk Which just gets us back to: it's not that * is bad, it's that it has inherent risks that you really have to think through, and thus shouldn't be offered as the default "just do this to fix it" solution.

This is one of the reasons the similar configs in CSP explicitly use the word unsafe when you're doing something unsafe. This is, IMO, significantly better DX, because the dev will hopefully be less likely to just copy and paste without understanding the risks.

It's unfortunate the Access-Control-Allow-* headers didn't use similarly evocative language.

@ricko thanks so much for that example! I would never have thought of that. i'm thinking about this * thing because (while I like to be as conservative as possible when setting permissions) I definitely had some wrong beliefs about why ACAO: * was bad and I like to know _why_ something is unsafe

I like the CSP naming and also with CSP it seems more obvious to me what the risks of allowing arbitrary code to run on my site are

@b0rk

I like to know why something is unsafe

Yeah, absolutely. I like the CSP stuff at https://content-security-policy.com/ because it includes lots of very practical guidance on when and when not to use each option. I think the CORS space could really use something similar, where it has very real-world scenarios.

"Do you want other sites to be able to use your images? Your JS?"

"Does your site offer embeddable content, such as via iframe?"

"Does your site allow users to upload content?"

"Is your site accessible via multiple domain names?"

etc, etc. It could then offer similar risk discussions and mitigation scenarios. "Always ensure images are actually images." "Never allow users to upload JS files." "Reject all SVG files with script tags."

Content-Security-Policy (CSP) Header Quick Reference

CSP or Content Security Policy Header Reference Guide and Examples

@ricko @b0rk

I assume that you mean that some other site might now include that malicious piece of js via <script src=foo> tag. If so, (a) doesn't that work in absence of A-C-A-O too? (b) in what scenarios an attacker can cause another site to embed that <script> tag, but neither an inline script tag, nor a <script src=bar> tag that points at any of the pastebin sites that exist on the web?

@robryk @b0rk

doesn't that work in absence of A-C-A-O?

As a script or img, yes. (Presuming poor CSP setup.) But we've also seen plenty of naive user agents which don't pay attention to the content-type and will instead content sniff. MSIE was a classic example. We're seeing similar vulnerabilities in AI agents.

in what scenarios an attacker can cause another site to embed that script tag, but neither an inline script tag, nor a script src=bar tag that points at any of the pastebin sites

I could see a scenario like this:

  • Someone creates a friends-of-mastodon.example
  • They set up the CSP to allow content from bad-cors.example because they want to use avatars or embedded toots.
  • They also allow users to submit content, such as their username.
  • They don't adequately sanitize one of the inputs, allowing script or img tags to slip through, like that username.
  • Now, simply viewing that username causes malicious JS to run, because it all looks "allowed".
  • @ricko @b0rk Your reply got truncated, presumably on the `<` character.

    @robryk @b0rk Updated. I brought it back.

    Amusingly, all the text I typed was still there. I'm now feeling very raised-eyebrow about the content sanitization in mastodon.

    @ricko @b0rk I would wager that this is the typical behaviour of markdown renderers, where selected html tags are passed through and others are skipped in some way.

    IMO a worse issue around rendering in fediverse is that a post can contain its contents in multiple formats (e.g. plain text, markdown, html...) and it's not entirely obvious which one will be rendered (so it can look differently depending on what does the rendering).

    Edit: Ah, and re making toots that appear differently to different people: also the fallback for quote toots is a good way to cause toots to appear differently on instances without quote toot support, not even mentioning the possibility of an instance sending different contents of the same toot to different inboxes.
    @robryk @b0rk Hahaha. Fair. All the pain of multi-part MIME. It's like we didn't learn the lesson the first time with HTML email.
    @ricko @b0rk And with mailing lists being able to sow confusion. (I am still surprised that I haven't seen sending-different-toots-to-different-instances used maliciously on fedi, only as part of "<your instance> is the best instance" jokes.)
    @robryk @b0rk hahahaha. There was an entire second half to that toot, which got chopped off because I didn't wrap the <script> tags in backticks. One sec, lemme retype it all.
    @ricko @b0rk

    But all that still works even with no A-C-A-O header anywhere, because it relies on <script src=foo>, no? I still don't see how including `A-C-A-O: *` makes things worse.

    @robryk @b0rk You're right, for that specific example. I ran out of characters and oversimplified.

    In today's world of SPAs, it's just as likely that this could be done without script tags, just some field where the user enters a URL. For example, "use this image at this URL as the cover art for this album" or "as the start for this generative AI thing".

    That URL gets fetched, sent back as JS (through poor sanitization on bad-cors.example), and then the * tells the browser to allow it to run.

    That still requires some exceptionally poor code on the fetch side of things, but that's always the case with vulnerabilities: they don't have to be likely, just possible, because the infinite monkeys which make up the Internet will eventually trip across the specific very bad combination.

    @ricko @b0rk

    So the situation is that the vulnerable side is doing cross-origin fetch from a user-provided URL, expecting an image/piece of text/something of that nature, gets a piece of JS or an SVG with embedded JS and (due to being vulnerable) runs it? And then the reason why it matters that it comes from _this particular_ site is that the user is providing the URL, so the attacker must find a non-suspicious looking one.

    Thanks, this is a scenario that does sound possible (and I have no good intuitions about what kinds of issues are likely to appear in today's SPAs; I only worked on non-web security).

    Also, this sounds similar to using (lack of) A-C-A-O as a "hotlinking prevention": lack of A-C-A-O here serves as a kind of weak repudiation of contents (it feels like CSP would benefit from a feature where a resource could be repudiated for CSP-related purposes).

    @robryk @b0rk Yeah, in a sense, A-C-A-O: * is effectively telling the world: "I attest anything accessible from bad-cors.example to be safe to use absolutely anywhere".

    Which is reasonable only when bad-cors.example has seriously vetted all the ways they expect their content to be used, and considered all the ways it might be abused.

    When you introduce user-managed content into that, those stories get significantly more complex. But if, for example, I wrote up some JS which I wanted to share with the world and I wanted to be usable anywhere, and it was always under my control, then * is perfectly fine and reasonable.

    @robryk @b0rk

    it feels like CSP would benefit from a feature where a resource could be repudiated for CSP-related purposes

    CSP does have a repudiation feature where you can really lock it down. Safelist instead of blocklist.

    https://content-security-policy.com/hash/

    Many modern build systems will generate these by default and embed them in the HTML which loads the JS for the app. (You can send them in the HTTP headers, but that's actually very rare, as now you need to synchronize your headers with specific content versions, which is a pain.)

    While these hashes are also useful for Sub-Resource Integrity (tamper detection), they work just as well for attestation. "When loading this app, these are the hashes of the exact list of scripts, css, images, etc, which I expect to be loaded and used."

    I just built a system for this a few months ago at my previous employer. It's fiddly to set up, but it really helps raise visibility of when something changes in your build/deploy system, whether intentional or not.

    CSP Hash Examples and Guide

    Implementing a hash with Content Security Policy (CSP)

    @ricko @b0rk I didn't mean that feature, but rather a response header that would mean "if you want to evaluate any CSP policy on this resource, it should fail".
    @b0rk I would appreciate if you can broadcast out what you find. I hear about “best practices” not to do it, but what are the attacks? Now if you try to do something like reflect the origin back so that you can send headers, that’s a different story :)
    @b0rk I agree. Like other legacy-caused rituals (meta color-scheme and meta viewport width, full rant at
    https://chaos.social/@chrysn/115988387148359810) it says "I'm not doing the weird stuff that you came to expect from web devs"; in this case, the weird stuff would be treating some requests differently (eg. without access control) just due to their network address.
    chrysn (@[email protected])

    @[email protected] And there's another line by which every modern dev has to greet the hat of browsers bowing to people's inability to follow standards: <meta name="color-scheme" content="dark light"> Way before smartphones and dark mode, I was taught by @[email protected] to not assume default colors, and thus to only set foreground colors I also set background colors -- in case someone has unconventional defaults. (Along with using percentages vs absolute lengths to work, and not assuming any DPI value).

    chaos.social
    @b0rk There's one scenario not mentioned in that post: if an authentication endpoint not protected by login CSRF responds with Access-Control-Allow-Origin: *, attackers may be able to mount a distributed brute-force attack against login. More info at https://security.stackexchange.com/questions/227779/concrete-example-of-how-can-access-control-allow-origin-cause-security-risks/254684#254684
    @b0rk I think this is just a specific case of Cross-Site Request Forgery? https://owasp.org/www-community/attacks/csrf
    Cross Site Request Forgery (CSRF) | OWASP Foundation

    Cross Site Request Forgery (CSRF) on the main website for The OWASP Foundation. OWASP is a nonprofit foundation that works to improve the security of software.

    @tedmielczarek ah yeah I think that's right thank you!

    @b0rk
    Off the top of my head check out "HTTP verbs and CSRF" on Wikipedia
    https://en.wikipedia.org/wiki/Cross-site_request_forgery

    (Though idk if CSRF is the right name for these attacks, it's got a nice ring to it)

    Cross-site request forgery - Wikipedia

    @b0rk I mean there is multiple different attacks that you can perform after that, and a name would depend on which attack is performed but if you want an umbrella term, one could suggest “Cross-Origin Data Exposure”. If you wanna be verbose and more specific, you could say “Cross site request forgery-assisted data theft”
    @b0rk ah i see i was too slow
    @b0rk I don’t think it’s CSRF, because if you set “Access-Control-Allow-Origin: *”, the browser doesn’t send credentials (which, I think, includes cookies).

    @b0rk It's reductive, but in general, don't ever set it to *.

    The one place where it's reasonable (but still not great) is multi-tenant hosting. That is, a single site/app which could be homed under any number of domains. In such a scenario, it may be more trouble than it's worth to try to have preflight responses check the Origin.

    If you know you aren't serving JS assets or anything which could reasonably be exploited, then, on paper at least, you can justify just using *.

    The trick with that, though, is if you are serving any user-uploaded content, then you can't 100% trust even that. Things like JS-in-SVG exploits are very real, as are things like the big WebP problem a few years back. You can try to rely on your media upload and handling pipeline to detect such problems, but you're then accepting some risk.

    Another place it's commonly used is embeddable content. But even that, I'd argue, should be handled on a dedicated domain. It's work, but it's worth it.

    @ricko interesting, do you think that the Mastodon API shouldn't set Access-Control-Allow-Origin: *? this is what I see when I look at Phanpy in the network tab

    @b0rk It's a fair question.

    I think the easiest way to answer would be to say: if I was undergoing a security review of my app (in this case Mastodon), and the auditors were very savvy, they would ask me to justify why I specifically used that configuration. I would want to have a decision record document on hand — or at the bare minimum a nice juicy comment in the code right above that * — explaining why it was set that way.

    For massive OSS like Mastodon, I would also want to be able to point devs at a similar document (maybe the same one). Not just to guide devs on that code, but also to guide devs using that code as a reference for developing something else. Or, increasingly, to get it in front of the LLMs which are going to tell devs to replicate that outcome.

    Maybe the Mastodon devs have a good, tested, reasonable explanation for using it. Maybe it's something I haven't considered.

    But if it's not documented, IMO, it's not really a reason, it's just an accident of inertia.

    @b0rk Looking at the Mastodon source, it seems like there's not a ton of explicit intent around CORS — it seems like it's just using the off-the-shelf config for Rack-CORS:

    https://github.com/mastodon/mastodon/blob/main/config/initializers/cors.rb

    Out of the box Mastodon only supports a single domain. An .env.production has a LOCAL_DOMAIN config which could be used to check the preflight Origin headers, instead of using *.

    But ... having said that, CORS is, as you have seen, very frustrating, and easy to misconfigure. Maybe they just don't want a zillion tickets from people trying to get their local instance running, but failing due to CORS misconfig.

    @b0rk yeah interesting. I don’t know that it is a problem. cookies/credentials can’t be sent (as part of the header) by browsers with the wildcard allow origin so you typically wouldn’t set that when you required an authenticated request.
    @b0rk If you're building an API for arbitrary other people to call from the front end it needs to have * (and is then authenticated through something like OAuth tokens)
    @b0rk seems fine if you have other ways of doing auth. e.g. any oauth-guarded thing has little reason to filter by caller.
    @groxx oh that makes sense, like it's fine if you're not using cookies for auth

    @b0rk I also feel like people forget that POST is fine cross-origin. You just can't read the response (beyond status code): https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS#simple_requests

    (This is why CSRF has been a thing since well before CORS came about)

    The rules around cross-origin POSTing are a bit complex / restricted nowadays, but the most common stuff has worked since the very earliest days of HTTP until now.

    Cross-Origin Resource Sharing (CORS) - HTTP | MDN

    Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources. CORS also relies on a mechanism by which browsers make a "preflight" request to the server hosting the cross-origin resource, in order to check that the server will permit the actual request. In that preflight, the browser sends headers that indicate the HTTP method and headers that will be used in the actual request.

    MDN Web Docs

    @groxx i find it so hard to remember which POSTs are allowed and which aren't (like an "application/x-www-form-urlencoded" content type is allowed but "application/json" isn't??)

    the rules feel arbitrary to me, kind of like they wanted to restrict POSTs more but couldn't because of backwards compatibility

    @b0rk very much that I think, yeah. but aside from the first time (per project because everyone always forgets the details immediately after) it's not like being restricted to form encoding is much of an issue, just make a "body" field and put json data in it
    @b0rk One situation when it's obviously safe is if any request made here could be made by the attacker directly: if there are no cookies that matter set on the domain, the server is on a publicly-available network, and nothing relies on discriminating by the client's IP, then the attacker doesn't gain anything by getting a victim to issue the request over doing that themselves.
    @robryk that makes sense, and I guess the Mastodon API is a good example of a server where that's true
    @b0rk Yes. I think a vast majority of APIs are like that.

    (It feels like CORS restrictions are sometimes used for a reason similar to the reason for protections against hotlinking: I know of public-data-returning APIs that don't allow cross-origin requests, so everyone who wants to build on top of them has to run a proxy of the API. Thus more people use the UI provided by the API provider, because there are fewer alternatives and they're harder to operate.)

    @robryk interesting, I hadn't thought of it that way.

    If that's true it does feel like it would contribute to a perception of same-origin restrictions as being "arbitrary"

    @b0rk

    Using these restrictions in a manner similar to hotlinking prevention is indistinguishable from a server on a private network using them to disallow other origins from accessing it via browsers of hapless users. I didn't think much about it, but I would assume that it wasn't possible to make one of these two possible without the other.

    I think the anti-hotlinking-like usage contributes to a perception of CORS as being something that reinforces boundaries in the web, including ones whose existence is not in the interest of the user. I'm not sure whether this makes them feel more arbitrary to me: what does that a lot more strongly is existence of no-cors mode.

    PS. Apologies: in my previous toots I implied that if you just set A-C-A-O then cross-origin requests will come with cookies. I just reread MDN article on CORS and it claims that unless you set more headers and the cross-origin requester explicitly asks for it, cross-origin requests will not include cookies. (And now here I feel like I'm seeing more arbitrariness.)

    @robryk one aspect of "hotlinking prevention" that just occurred to me though is that some APIs are not really "appropriate" to use in a frontend context

    like I don't think it would make sense to use the Twilio API in a frontend context, because of the way its auth works, you wouldn't want to expose your API key in your frontend in that way

    so if I was the developer of that API I might not set A-C-A-O to discourage that kind of usage? Not sure.

    @b0rk

    It might be appropriate for prototypes or some weird setups where e.g. the user behind the browser is the one who provides the API keys. I agree that someone might want to cut down on inappropriate uses of that and consider these to be acceptable collateral damage.

    That said, I suspect that the default (when no A-C-A-O is provided) being restrictive plays a large role here.
    @b0rk the wildcard isn’t as bad as it sounds. It only allows anonymous requests(=no cookies). If you want to be truly permissive, you need to reflect the incoming Origin header.
    @freddy that does make me feel even less clear about why it would not be safe to set Access-Control-Origin: * though
    @b0rk In my head, the feature is similar to the CSP frame-ancestors directive. It is not really solving a direct security issue but it is preemptively blocking malicious websites from misusing your website. For example, a website could be a full-screen iframe of your website, but also add a popup box that asks for donations to your page, which then ends in a scammer's pockets. By blocking acces to your website resources, you make it significantly harder to pull off such a scam.

    @b0rk @freddy My understanding is that it is designed to be safe to set for any site that is reachable from the public Internet.

    In other words, a bunch of early browser security model decisions were made to prevent exfiltration of data from behind corporate firewalls.

    ACAO:* is designed to be the thing that you can set globally in the server configuration for servers that are public, to fix the things that don't need to be blocked for public servers.

    @b0rk @freddy I think the canonical historical reference for this being the intent is https://annevankesteren.nl/2012/12/cors-101
    @dbaron @b0rk thanks for finding this article. My recollection is also that it’s meant to support private networks (home routers etc) snd generally align with previous security models (back compat).