CORS security hole

Hi, 

  Two things came together to make me notice the problem I want to discuss here:

  1. On the read-write-web and WebID community groups we were discussion the possibility of delegated authorisation with WebID [1]
  2. I was building a CORS proxy for linked data javascript agents (see the forwarded message )

Doing this I ended up seeing some very strong parallels between CORS and WebID delegation. One could
say that they are nearly the same protocol except that WebID delegation uses a WebID URL to identify an agent, rather than the CORS Origina protocol:hostname:port service id. The use case for WebID authorisation delegation is also different.

 Having worked out the parallel I was able to start looking into the security reasoning between CORS. And this is where I found a huge security hole ( I think ). Essentially if you allow authenticated CORS requests, then any javascript agent can use a doggy CORS proxy, and use that to fool any browser into thinking the server was aware that it was not the user directly it was communicating with but the javascript agent. 

 This is possible because CORS authenticated delegation with cookies is hugely insecure. CORS authenticated delegation over TLS would not suffer from this problem, as the proxy could not pass itself off as the browser user.

  Another reason to use TLS, another reason to use WebID.

	Henry


[1] http://www.w3.org/wiki/WebID/Authorization_Delegation using http://webid.info/

Begin forwarded message:
> From: Henry Story <henry.story@bblfish.net>
> Subject: Re: CORS Proxy
> Date: 7 July 2012 08:37:24 CEST
> To: Read-Write-Web <public-rww@w3.org>
> Cc: WebID <public-webid@w3.org>, Joe Presbrey <presbrey@gmail.com>, Mike Jones <mike.jones@manchester.ac.uk>, Romain BLIN <romain.blin@etu.univ-st-etienne.fr>, Julien Subercaze <julien.subercaze@univ-st-etienne.fr>
> 
> 
> On 7 Jul 2012, at 07:35, Henry Story wrote:
> 
>> 
>> On 6 Jul 2012, at 23:10, Henry Story wrote:
>> 
>>> Hi,
>>> 
>>> I just quickly put together a CORS Proxy [1], inspired by Joe Presbrey's data.fm CORS proxy [2].
>>> 
>>> But first, what is a CORS proxy?
>>> --------------------------------
>>> 
>>> A CORS [3] proxy is needed in order to allow read-write-web pages containing javascript agents written with libraries such as rdflib [5] to fetch remote resources. Pages containing such javascript agents are able to fetch and parse RDF on the web, and thus crawl the web by following their robotic nose. A CORS Proxy is needed here because:
>>> 
>>> 1- browsers restrict which sites javascript agents can fetch data from to those from which the javascript came from - the famous "same origin policy" ( javascript can only fetch resources from the same site it originated from)
>>> 2- CORS allows an exception to the above restriction, if the resource has the proper headers. For a GET request this is the Access-Control-Allow-Origin header
>>> 3- most RDF resources on the web are not served with such headers
>>> 
>>> Hence javascript agents running inside web browsers that need to crawl the web, need a CORS proxy, so that libraries such as rdflib can go forward and make those requests through the proxy. In short: a CORS proxy is a service that can forward the request to the appropriate server and on receiving the answer add the correct headers, if none were found.
>>> 
>>> Security
>>> --------
>>> 
>>> So is there a security problem having a CORS proxy make a GET request for some information on behalf of JS Agent? This is an important question, because otherwise we'd be introducing a security hole with such a proxy.
>>> 
>>> In order to answer that question we need to explain why browsers have the same origin restriction.
>>> 
>>> The answer is quite simple I think. A Javascript agent running in a browser is using the credentials of the user when it makes requests for resources on the web. One can therefore think of the browser as acting as a secretary for the javascript agent: the JS agent makes a request, but does not log in to a web site, but instead asks the browser to fetch the information. The browser uses its authentication credentials - the user of the browser's credentials to be precise - to connect to remote sites and request resources. The remote site may be perfectly fine with the browser user/owner having access to the resource, but not like the idea of the agent in the browser doing so. (after all that could be some JS on some random site the user came across) In order to avoid this danger, the browser sends along with its requests an Origin: header and the URL of the host where the javascript was found. The service receiving such a request must respond with an Access-Control-Allow-Origin header to make clear that it is ok with the JS Agent receiving this information.
>>> IF the browser finds out that the web site allows the JS Agent to receive the information too, then it will pass the information on to it.
>>> 
>>> This is a bit like what we discussed about a secretary agent on a web site requesting a resource On-Behalf-Of a user in our WebID delegation [5] . Here the Browser is the secretary, and the JS agent is the user being acted on behalf of. The difference is that the Secretary/Browser in this case is well known, and the JS Agent is the unknown; or put another way in the CORS case the server's authorisation policies were geared towards giving the browser owner access and not for the JS it is acting on behalf of, whereas in our delegation use case we were imagining the remote resource not being initially authorised directly to the secretary, but rather for the agent she was acting on behalf of. (small shift in focus)
>>> 
>>> Anonymous Proxy
>>> 
>>> So now it should be clear why using the proxy is not creating a new security issue. This is because the Proxy is not the Browser and so has NOT authenticated as the user when it is making a request. The Proxy is making an anonymous request to a remote resource. This remote resource is therefore public. As such it is fine to allow any JS agent to read it. This is particularly true of GET requests. But it should even be true of POST, PUT and DELETE requests. If those are public and allow anonymous usage, then it should be possible for a CORS proxy to do that on behalf of a javascript. It is quite possible that the CORS proxy might want to authenticate the user in order to not become a vector for denial of service attacks, and it could even give users a history of requests it made. Unless perhaps some people are placing identification cookies in URLs! (But one could argue that's their problem?!)
>> 
>> Ah, there is an issue here I just realised. Currently my proxy is passing along all the headers sent to 
>> it by the client.  Well all minus the "Accept" headers, because my Proxy only understands a subset of RDF
>> serialisations. ( And the proxy has to interpret the rdf in the serialisation in order to transform relative
>> URIs into full URIs.  The proxy we are building is not a real HTTP Proxy, because those would require a browser to be set up correctly and doing that would probably not be possible on a case by case basis )
>> 
>> 
>>  case CORSFetch(url, headers) => {
>>    val hdrs = for (key <- (headers - "Accept").keys;
>>                   value <- (headers(key))) yield (key,value)
>>    val promiseIteratee: Promise[Iteratee[Array[Byte], Either[CORSException, CORSResponse[Rdf]]]] =
>>      WS.url(url.toExternalForm).
>>        withHeaders(hdrs.toSeq:_*).
>>        withHeaders("Accept" -> "application/rdf+xml,text/turtle,application/xhtml+xml;q=0.8,text/html;q=0.7,text/n3;q=0.2").
>>        get {
>>        response: ResponseHeaders =>
>>               ....
>> 
>> So here all the headers - "Accept" are passed along. But that means cookies are passed along too!
>> And if cookies are passed along, then the user's identifications are passed along, and that would be
>> problematic, for the reasons given above. 
>> 
>> Luckily as explained by this CORS article
>> https://developer.mozilla.org/en/http_access_control#Requests_with_credentials
>> 
>> unless the javascript makes the XMLHttpRequest with the withCredentials property set to true, cookies are not sent. Also - even then - unless the server in its response places the header
>> 
>>      Access-Control-Allow-Credentials: true
>> 
>> in the header the browser will not pass the credentials on to the javascript (and it also has to be explicitly about which origin it allows)
>> 
>> But this raises another issue: it would be extremely easy for me to change my proxy to add those headers to the response. If so this means that any evil script, can communicate with such a slightly enhanced proxy that does the transformation described. And so CORS is protecting nothing seriously here.
> 
> An evil proxy could also - come to think of it - change the headers for the Origin of the request to one that any bank would feel comfortable with, so that with this system in place a service that would allow access to some Origin, would allow access to all of them (via evil proxies).
> 
> Interestingly enough, this attack does not work with WebID over TLS authentication. The Proxy cannot know the browser's private key, and so cannot authenticate to the remote service other than by using its own certificate and WebID. Identity here is not passed by a spoofable cookie, but as a cryptographically verified TLS session.
> 
> In a secure WebID world one may still want the proxy to act on behalf of the browser owner. This could be done if the Proxy specified that it was acting on behalf of the user of the browser using WebID Authorization Delegation [5]. This would be something that the resource serving service could both verify and evaluate.
> 
> 
>> 
>> 
>> 
>>> 
>>> Authenticated Proxies
>>> 
>>> Now let us assume we have CORS proxies that can also authenticate with WebID. If they did so, then they would have to follow the same rules as the Browser: they should not pass on the information unless the server had allowed them to by setting the correct Access-Control-Allow-Origin headers, and allowing the javascript access, since the information they were given was not meant for this other JS agent.
>>> 
>>> But things could get a little more advanced yet: Imagine that the proxy authenticated itself on behalf of the user who made the request. The server serving the resource could then verify that the proxy was allowed to do act on behalf of the user using the procedure outlined in WebID delegation [5]. Then we would have to deal with WebID delegation plus CORS delegation. The server would know that the proxy was acting on behalf of the user, and that the user was acting on behalf of the JS Agent... This may be the use case we had trouble putting our finger on at this weeks WebID teleconf...
>>> 
>>> 
>>> Improving CORS with LinkedData
>>> ------------------------------
>>> 
>>> Here is an idea to improve CORS: In WebID delegation we found a way to let the server know what relation the secretary had to the agent she acted on behalf of. That Agent she was acting on behalf of could add a 
>>> 
>>> :me cert:secretaty myprofile:secr .
>>> 
>>> relation to his profile. This would help the server know what relation the secretary had to the agent she claimed to be working for.
>>> 
>>> With CORS this relationship is nowhere made explicit (as far as I can see): there is no way for me to tell a web site that the javascript I am using ( and that is hosted on my freedom box) is something I would like the server I am connecting to trust, as opposed to some javascript that just started executing itself on some server I came across. This makes me thing that
>>> 1. it would be useful if JS could be signed so as to have a better identity than just the identity of a whole site
>>> 2. we could create a relation such as the cert:secretary one that would allow me in my profile to say for example
>>> 
>>> :me cert:trustedJS <https://bblfish.net> .
>>> 
>>> IT's really much to vague, but it would make it easier for people to trust my JS.
>>> 
>>> 
>>> OTHER TODOS
>>> -----------
>>> 
>>> There are really quite a lot of details questions left open for CORS proxies. Should a CORS proxy return a 203? When? Who should it deal with error messages? .... I think we should organise those on a wiki page, and keep the discussion alive. My code at present was put together really quickly, but it made me ask a lot of questions whilst writing it.
>>> 
>>> 
>>> 	Henry
>>> 
>>> 
>>> [1] the code is here https://github.com/bblfish/Play20/blob/webid/framework/src/webid/src/main/scala/org/w3/readwriteweb/play/CORSProxy.scala
>>> But I have not placed it online yet.
>>> [2] Joe's proxy is online and available by sending GET requests here: http://data.fm/proxy?uri={uri}
>>> and the code is somewhere here: https://github.com/linkeddata/data.fm
>>> [3] http://www.w3.org/TR/cors/
>>> [4] https://github.com/linkeddata/rdflib.js
>>> [5] http://www.w3.org/wiki/WebID/Delegation
>>> 
>>> Social Web Architect
>>> http://bblfish.net/
>>> 
>> 
>> Social Web Architect
>> http://bblfish.net/
>> 
> 
> Social Web Architect
> http://bblfish.net/
> 

Social Web Architect
http://bblfish.net/

Received on Monday, 16 July 2012 15:19:15 UTC