Michael Krotscheck
![]() |
![]() |
Netscape 2 (1995)
A user agent (browser) permits scripts contained in a first web page, to access data in a second web page, but only if both web pages have the same origin.
A combination of URI scheme, hostname, and port.
http://www.example.com:8080/script.js
<script src="https://malicious.com/keystrokelogger.js" ></script>
<iframe src="https://amazon.com/all_my_credit_card_info.html"
width="100%" height="100%" ></iframe>
Directly including scripts
<script src="https://api.example.com/script.js" ></script>
Form POST
<form method="POST" action="https://api.example.com/action" ></form>
"...that was the easiest excuse for shipping it so I needed to cram XML into the name."Alex Hopmann
Internet Explorer 5 (1999)
Outlook Web Access (Exchange Server 2000)
// http://myapp.example.com/scripts.js
var httpget = new XMLHttpRequest();
httpget.open("GET", "https://myapp.example.com/api/v1/thing", true);
httpget.send();
var httppost = new XMLHttpRequest();
httppost.open("POST", "https://myapp.example.com/api/v1/thing", true);
httppost.send("{}");
GET /api/v1/thing HTTP/1.1
Host: myapp.example.com
POST /api/v1/thing HTTP/1.1
Host: myapp.example.com
The same origin policy was extended to include XMLHttpRequest.
// http://myapp.example.com/scripts.js
// This doesn't work.
xmlhttprequest.open("GET", "https://api.example.com/v1/users/me.json");
// This doesn't work either
xmlhttprequest.open("GET", "https://api.square.com/billing/cards.json");
// https://mobile.example.com/scripts.js
// Nor does this.
xmlrequest.open("GET", "https://api.example.com/v1/users/me.json");
// https://tablet.example.com/scripts.js
// Or this.
xmlrequest.open("GET", "https://api.example.com/v1/users/me.json");
The terrible things we did.
// https://myapp.example.com/app.js
xmlhttprequest.open("GET", "https://myapp.example.com/square/billing/cards.json");
# pythonapp/proxy/square.py
import urllib2
def proxyGet(fragment):
urllib2.urlopen('https://api.square.com/%s' % (fragment,)).read();
// Example response that would require an intelligent proxy.
[
{'ref': 'https://api.square.com/billing/cards/1234.json',
'type': 'link'},
{'ref': 'https://api.square.com/billing/cards/2231.json',
'type': 'link'}
]
HTML DOM 2.0 (read only), January 2003
<!-- http://myapp.example.com/index.html -->
<iframe src="https://orders.example.com/iframe.html"></iframe>
<iframe src="https://products.example.com/iframe.html"></iframe>
document.domain = "example.com"
JSON With Padding (2006)
<script type="application/javascript"
src="https://api.square.com/billing/cards.js?callback=receiver">
</script>
receiver({"Name": "Foo", "Id": 1});
https://www.whatwg.org (ca 2008)
// https://orders.example.com/childIframe.html
window.addEventListener('message', function receiver(event) {
alert(event.data);
}, false);
// https://myapp.example.com/index.html
var childframe = document.getElementsByTagName('iframe')[0];
childframe.contentWindow
.postMessage('message_content', 'https://myapp.example.com/');
RFC 6455 (2011)
Optionally, an |Origin| header field. This header field is sent by all browser clients.
January 2014
Browser Request
OPTIONS /billing/cards HTTP/1.1
Host: api.example.com
Origin: https://myapp.example.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, x-client
Permissive Response
HTTP/1.0 200 OK
Vary: Origin
Access-Control-Allow-Origin: https://myapp.example.com
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: accept,x-client
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600
Browser Request
GET /billing/cards HTTP/1.1
Host: api.example.com
Origin: https://myapp.example.com
Accept: application/json
X-Client: myapp
Permissive Response
HTTP/1.0 200 OK
Content-Type: application/json; charset=UTF-8
Vary: Origin
Access-Control-Allow-Origin: https://myapp.example.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Accept,Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma
HTTP/1.0 200 OK
Content-Type: application/json; charset=UTF-8
Vary: Origin
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Accept,Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma
http://git.openstack.org/cgit/openstack/oslo.middleware/tree/oslo_middleware/cors.py
class CORS(base.Middleware):
"""CORS Middleware.
This middleware allows a WSGI app to serve CORS headers for multiple
configured domains.
For more information, see http://www.w3.org/TR/cors/
"""
simple_headers = [
'Content-Type',
'Cache-Control',
'Content-Language',
'Expires',
'Last-Modified',
'Pragma'
]
def __init__(self, application, conf=None):
super(CORS, self).__init__(application)
# Begin constructing our configuration hash.
self.allowed_origins = {}
def add_origin(self, allowed_origin, allow_credentials=True,
expose_headers=None, max_age=None, allow_methods=None,
allow_headers=None):
'''Add another origin to this filter.
:param allowed_origin: Protocol, host, and port for the allowed origin.
:param allow_credentials: Whether to permit credentials.
:param expose_headers: A list of headers to expose.
:param max_age: Maximum cache duration.
:param allow_methods: List of HTTP methods to permit.
:param allow_headers: List of HTTP headers to permit from the client.
:return:
'''
if allowed_origin in self.allowed_origins:
LOG.warn('Allowed origin [%s] already exists, skipping' % (
allowed_origin,))
return
self.allowed_origins[allowed_origin] = {
'allow_credentials': allow_credentials,
'expose_headers': expose_headers,
'max_age': max_age,
'allow_methods': allow_methods,
'allow_headers': allow_headers
}
def process_response(self, response, request=None):
'''Check for CORS headers, and decorate if necessary.
Perform two checks. First, if an OPTIONS request was issued, let the
application handle it, and (if necessary) decorate the response with
preflight headers. In this case, if a 404 is thrown by the underlying
application (i.e. if the underlying application does not handle
OPTIONS requests, the response code is overridden.
In the case of all other requests, regular request headers are applied.
'''
# Sanity precheck: If we detect CORS headers provided by something in
# in the middleware chain, assume that it knows better.
if 'Access-Control-Allow-Origin' in response.headers:
return response
# Doublecheck for an OPTIONS request.
if request.method == 'OPTIONS':
return self._apply_cors_preflight_headers(request=request,
response=response)
# Apply regular CORS headers.
self._apply_cors_request_headers(request=request, response=response)
# Finally, return the response.
return response
def _split_header_values(self, request, header_name):
"""Convert a comma-separated header value into a list of values."""
values = []
if header_name in request.headers:
for value in request.headers[header_name].rsplit(','):
value = value.strip()
if value:
values.append(value)
return values
def _apply_cors_preflight_headers(self, request, response):
"""Handle CORS Preflight (Section 6.2)
Given a request and a response, apply the CORS preflight headers
appropriate for the request.
"""
# If the response contains a 2XX code, we have to assume that the
# underlying middleware's response content needs to be persisted.
# Otherwise, create a new response.
if 200 > response.status_code or response.status_code >= 300:
response = webob.response.Response(status=webob.exc.HTTPOk.code)
# Does the request have an origin header? (Section 6.2.1)
if 'Origin' not in request.headers:
return response
# Is this origin registered? (Section 6.2.2)
origin = request.headers['Origin']
if origin not in self.allowed_origins:
if '*' in self.allowed_origins:
origin = '*'
else:
LOG.debug('CORS request from origin \'%s\' not permitted.'
% (origin,))
return response
cors_config = self.allowed_origins[origin]
# If there's no request method, exit. (Section 6.2.3)
if 'Access-Control-Request-Method' not in request.headers:
LOG.debug('CORS request does not contain '
'Access-Control-Request-Method header.')
return response
request_method = request.headers['Access-Control-Request-Method']
# Extract Request headers. If parsing fails, exit. (Section 6.2.4)
try:
request_headers = \
self._split_header_values(request,
'Access-Control-Request-Headers')
except Exception:
LOG.debug('Cannot parse request headers.')
return response
# Compare request method to permitted methods (Section 6.2.5)
if request_method not in cors_config['allow_methods']:
LOG.debug('Request method \'%s\' not in permitted list: %s'
% (request_method, cors_config['allow_methods']))
return response
# Compare request headers to permitted headers, case-insensitively.
# (Section 6.2.6)
for requested_header in request_headers:
upper_header = requested_header.upper()
permitted_headers = (cors_config['allow_headers'] +
self.simple_headers)
if upper_header not in (header.upper() for header in
permitted_headers):
LOG.debug('Request header \'%s\' not in permitted list: %s'
% (requested_header, permitted_headers))
return response
# Set the default origin permission headers. (Sections 6.2.7, 6.4)
response.headers['Vary'] = 'Origin'
response.headers['Access-Control-Allow-Origin'] = origin
# Does this CORS configuration permit credentials? (Section 6.2.7)
if cors_config['allow_credentials']:
response.headers['Access-Control-Allow-Credentials'] = 'true'
# Attach Access-Control-Max-Age if appropriate. (Section 6.2.8)
if 'max_age' in cors_config and cors_config['max_age']:
response.headers['Access-Control-Max-Age'] = \
str(cors_config['max_age'])
# Attach Access-Control-Allow-Methods. (Section 6.2.9)
response.headers['Access-Control-Allow-Methods'] = request_method
# Attach Access-Control-Allow-Headers. (Section 6.2.10)
if request_headers:
response.headers['Access-Control-Allow-Headers'] = \
','.join(request_headers)
return response
def _apply_cors_request_headers(self, request, response):
"""Handle Basic CORS Request (Section 6.1)
Given a request and a response, apply the CORS headers appropriate
for the request to the response.
"""
# Does the request have an origin header? (Section 6.1.1)
if 'Origin' not in request.headers:
return
# Is this origin registered? (Section 6.1.2)
origin = request.headers['Origin']
if origin not in self.allowed_origins:
LOG.debug('CORS request from origin \'%s\' not permitted.'
% (origin,))
return
cors_config = self.allowed_origins[origin]
# Set the default origin permission headers. (Sections 6.1.3 & 6.4)
response.headers['Vary'] = 'Origin'
response.headers['Access-Control-Allow-Origin'] = origin
# Does this CORS configuration permit credentials? (Section 6.1.3)
if cors_config['allow_credentials']:
response.headers['Access-Control-Allow-Credentials'] = 'true'
# Attach the exposed headers and exit. (Section 6.1.4)
if cors_config['expose_headers']:
response.headers['Access-Control-Expose-Headers'] = \
','.join(cors_config['expose_headers'])