Fixing CORS Security Holes

I recently received an email alerting me to a security vulnerability in Bloopist, my custom blogging platform. The security vulnerability would have allowed attackers to impersonate other users on Bloopist after getting them to visit a web page containing malicious JavaScript. I have no proof that this was done, and the security vulnerability is now closed. The root cause was a CORS misconfiguration.

Want to learn more about CORS? Check out CORS in Action: Creating and consuming cross-origin APIs.

Thanks to Jens Mueller, I was alerted to this issue. He was kind enough to send me the email below:

*Please forward to tech/developer/security team*

TL;DR  +++ bloopist.com generates the 
Access-Control-Allow-Origin header in a dangerous way +++ 
websites like bloopist.com.evil.com are allowed CORS access +++ 
leads to SOP bypass (aka completely taking over accounts) and 
SSL bypass +++ Fix: disable CORS dynamic header generation or 
re-config to trusted URLs +++

- Affected service: bloopist.com
- OWASP Top 10 category: A5 (Security Misconfiguration)
- Impact: Take over user accounts, SSL/TLS bypass

Dear bloopist.com security team,

In the scope of academic research on web security, we touched 
upon a vulnerability in bloopist.com. The website uses 
Cross-Origin Resource Sharing (CORS) in an insecure way:


*** Weakness: Post-domain wildcard origin reflection ***

bloopist.com's *Access-Control-Allow-Origin* header is 
dynamically generated based on the browser's *Origin: ...* 
request header. The generation code however only checks if the 
Origin *starts* with bloopist.com. Furthermore, the 
*Access-Control-Allow-Credentials* header is present. This 
allows an attacker to bypass access controls such as the 
same-origin policy. For example, a malicious website like 
bloopist.com.evil.com visited by a client logged into 
bloopist.com can perform actions in the context of the logged in 
user on bloopist.com. Furthermore, this allows a MitM attacker 
to bypass SSL encryption.


*** Exploit: SSL-bypass (scenario: MitM attacker) ***

/A http origin is allowed CORS access to a https resource,/
/this allows a man-in-the-middle to break https encryption/

https://bloopist.com allows CORS-access from non-encrypted 
origins like "Origin: http://bloopist.com". This enables a 
man-in-the-middle to practically bypass SSL encryption: The 
attacker just has to wait until the victim visits *any* 
unencrypted website, insert a redirect to a fake 
http://bloopist.com.whatever site she set up -- and then embed 
JavaScript code here, which fetches the user's data from 
https://bloopist.com. Now -- via CORS -- she can access the SSL 
encrypted content of https://bloopist.com/some-private-user-info 
or perform arbitrary actions in the context of the logged in 
user. Of course, a MitM is a strong attacker model. However, 
it's what SSL actually was supposed to protect from.


More details on CORS-misconfiguration issues can be found here:
https://web-in-security.blogspot.de/2017/07/cors-misconfigurations
-on-large-scale.html
http://blog.portswigger.net/2016/10/exploiting-cors-misconfigurati
ons-for.html
https://www.youtube.com/watch?v=wgkj4ZgxI4c

Proof-of-concept code can be sent on request.
Feel free to contact me for any questions.

Greetings,
Jens Mueller


--
Chair for Network and Data Security, Ruhr-University
M.Sc. Jens Mueller
Universitaetsstr. 150
Building ID 2/469
D-44780 Bochum
Phone: +49 (0) 234 / 32-29177

Basically, the regular expressions I was using to allow CORS access were broken. I needed to modify them to only allow https access, and ensure that only domains that ended in bloopist.com should be allowed.

Testing for CORS Vulnerabilities

To test which origins were allowed by my CORS configuration, I used a simple curl command:

curl -H "Origin: https://bloopist.com.evil.com" --verbose https://bloopist.com 2>&1 | grep Origin

This command let me pick the origin that I wanted to pretend to be and printed out any header lines that included the word "Origin". From that, I could see if the server was sending back an Access-Control-Allow-Origin string that would allow attacks from other domains or not.

Before implementing my fixed CORS whitelist, I'd get outputs from curl like the one below.

$ curl -H "Origin: https://bloopist.com.evil.com" --verbose https://bloopist.com 2>&1 | grep Origin
> Origin: https://bloopist.com.evil.com
< Access-Control-Allow-Origin: https://bloopist.com.evil.com
< Vary: Origin

Protecting CORS Access

I updated my CORS regular expression to force https, allow either a subdomain of bloopist.com or the bare domain by itself, and to force the origin to end with bloopist.com. I use Rack::Cors in my Ruby on Rails applications, and I configured it like this:

# Access-Control-Allow-Origin
config.middleware.insert_before 0, Rack::Cors do
    allow do
        if Rails.env == "development"
            origins('*')
        elsif Rails.env == "production"
            origins(/\Ahttps:\/\/(.*?\.|)bloopist\.com\z/)
        end
        resource '*', :headers => :any, :methods => :any
    end
end

Results

After updating my configuration, I double checked which origins were allowed CORS access. Domains other than bloopist.com are no longer allowed access. However, I was unable to figure out how to make curl indicate that it was coming from the null origin, so I was unable to test that that particular vulnerability was closed.

Attacker Origin Allowed Before? Allowed After?
https://bloopist.com Yes Yes
https://blog.bloopist.com Yes Yes
https://bloopist.com.evil.com Yes No
http://bloopist.com Yes No
null ? ?

Do you know how to get curl to use the null origin? Did you find this information helpful? Let me know in the comments.