Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions examples/apps/api-gateway-multiple-origin-cors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# cors-multiple-origin

*Example of Multiple-Origin CORS using API Gateway and Lambda*

[Cross-Origin Resource Sharing (CORS) - MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)

### Local development

First, [set up the SAM CLI](https://github.com/awslabs/aws-sam-cli#installation).

Now, test the application locally using:

`sam local start-api`

Note that there was an [issue](https://github.com/awslabs/aws-sam-cli/issues/400) that prevented OPTIONS requests from being handled when running with the SAM CLI version 0.3.0. This does not occur when the application is deployed.

Run the tests:

`npm install`

`npm test`

### Deploying

```bash
sam package \
--template-file template.yaml \
--output-template-file packaged.yaml \
--s3-bucket $YOUR_BUCKET_NAME
```

```bash
sam deploy \
--template-file packaged.yaml \
--stack-name cors-multiple-origin \
--capabilities CAPABILITY_IAM
```

### Getting the URL of the deployed instance

```bash
aws cloudformation describe-stacks \
--stack-name cors-multiple-origin \
--query 'Stacks[].Outputs'
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"allowedOrigins": [
"http://127.0.0.1",
"https://*.example.com",
"https://*.amazon.com"
]
}
81 changes: 81 additions & 0 deletions examples/apps/api-gateway-multiple-origin-cors/cors-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// this is the list of headers allowed by default by the API Gateway console
// see: https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors.html
// and: https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonRequestHeaders.html
const DEFAULT_ALLOWED_HEADERS = [
"Content-Type", // indicates the media type of the resource
"X-Amz-Date", // the current date and time according to the requester (must be present for authorization)
"Authorization", // information required for request authentication
"X-Api-Key", // an AWS API key
"X-Amz-Security-Token" // see link above
];
exports.DEFAULT_ALLOWED_HEADERS = DEFAULT_ALLOWED_HEADERS;

/**
* Extract the Origin header from a Lambda event
* @param event Lambda event
*/
exports.getOriginFromEvent = event => event.headers.Origin || event.headers.origin;

/**
* Return an object that contains an Access-Control-Allow-Origin header
* if the request origin matches a pattern for an allowed origin.
* Otherwise, return an empty object.
* @param {String} origin the origin to test against the allowed list
* @param {Array} allowedOrigins A list of strings or regexes representing allowed origin URLs
* @return {Object} an object containing allowed header and its value
*/
exports.createOriginHeader = (origin, allowedOrigins) => {
if (!origin)
return {}; // no CORS headers necessary; browser will load resource

// look for origin in list of allowed origins
const allowedPatterns = allowedOrigins.map(exports.compileURLWildcards);
const isAllowed = allowedPatterns.some(pattern => origin.match(pattern));
if (isAllowed)
return {"Access-Control-Allow-Origin": origin};

// the origin does not match any allowed origins
return {}; // return no CORS headers; browser will not load resource
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. This is all much simpler.

// we do not return a "null" origin because this is exploitable
};

/**
* Return an object that contains a preflight response to be returned
* from a Lambda function.
* @param {String} origin the origin to test against the allowed list
* @param {Array} allowedOrigins A list of strings or regexes representing allowed origin URLs
* @param {Array} allowedMethods a list of strings representing allowed HTTP methods
* @param {Array} allowedHeaders (optional) a list of strings representing allowed headers
* @param {Number} maxAge (optional) time in seconds until preflight response expires
* @return {Object} an object containing several header => value mappings
*/
exports.createPreflightResponse = (origin, allowedOrigins, allowedMethods, allowedHeaders = DEFAULT_ALLOWED_HEADERS, maxAge) => {
let headers = Object.assign(exports.createOriginHeader(origin, allowedOrigins), {
"Access-Control-Allow-Headers": allowedHeaders.join(","),
"Access-Control-Allow-Methods": allowedMethods.join(",")
});
if (maxAge !== undefined)
headers["Access-Control-Max-Age"] = maxAge;
return {headers, statusCode: 204};
};

/**
* Compiles a URL containing wildcards into a regular expression.
*
* Builds a regular expression that matches exactly the input URL, but allows
* any number of URL characters in place of each wildcard (*) character.
* http://*.example.com matches http://abc.xyz.example.com but not http://example.com
* http://*.example.com does not match http://example.org/.example.com
* @param {String} url the url to compile
* @return {RegExp} compiled regular expression
*/
exports.compileURLWildcards = (url) => {
// unreserved characters as per https://tools.ietf.org/html/rfc3986#section-2.3
const urlUnreservedPattern = "[A-Za-z0-9\-._~]";
const wildcardPattern = urlUnreservedPattern + "*";

const parts = url.split("*");
const escapeRegex = str => str.replace(/([.?*+^$(){}|[\-\]\\])/g, "\\$1");
const escaped = parts.map(escapeRegex);
return new RegExp("^" + escaped.join(wildcardPattern) + "$");
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const cors = require("./cors-util");

// createOriginHeader
test("use createOriginHeader to make a header for no origin", () => {
const result = cors.createOriginHeader(undefined, []);
expect(result).toEqual({});
});

test("use createOriginHeader to make a header for a single origin", () => {
const origin = "https://amazon.com";
const allowedOrigins = [origin];
const result = cors.createOriginHeader(origin, allowedOrigins);
expect(result).toEqual({"Access-Control-Allow-Origin": origin});
});

test("use createOriginHeader to make a header for one of several origins", () => {
const origin = "https://amazon.com";
const allowedOrigins = ["https://example.com", origin, "http://amazon.com"];
const result = cors.createOriginHeader(origin, allowedOrigins);
expect(result).toEqual({"Access-Control-Allow-Origin": origin});
});

test("use createOriginHeader to make a header for a disallowed origin", () => {
const origin = "https://not-amazon.com";
const allowedOrigins = [];
const result = cors.createOriginHeader(origin, allowedOrigins);
expect(result).toEqual({});
});

test("use createOriginHeader to make a header for a disallowed origin", () => {
const origin = "https://not-amazon.com";
const allowedOrigins = ["https://example.com", "https://amazon.com", "http://amazon.com"];
const result = cors.createOriginHeader(origin, allowedOrigins);
expect(result).toEqual({});
});

// createPreflightResponse
test("use createPreflightResponse to make CORS preflight headers", () => {
const origin = "https://amazon.com";
const allowedOrigins = [origin];
const allowedMethods = ["CREATE", "OPTIONS"];
const allowedHeaders = ["Authorization"];
const maxAge = 8400;
const result = cors.createPreflightResponse(origin, allowedOrigins, allowedMethods, allowedHeaders, maxAge);
expect(result).toEqual({
headers: {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "CREATE,OPTIONS",
"Access-Control-Allow-Headers": "Authorization",
"Access-Control-Max-Age": 8400
},
statusCode: 204
});
});

// compileURLWildcards
test("compile pattern with no wildcards", () => {
const pattern = "https://amazon.com";
const regex = cors.compileURLWildcards(pattern);
expect(pattern).toMatch(regex);
expect("https://example.com").not.toMatch(regex);
});

test("test pattern with wildcard", () => {
const pattern = "https://*";
const regex = cors.compileURLWildcards(pattern);
expect("https://example.com").toMatch(regex);
});

test("test pattern with subdomain wildcard", () => {
const pattern = "https://*.amazon.com";
const regex = cors.compileURLWildcards(pattern);
expect("https://restaurants.amazon.com").toMatch(regex);
expect("https://amazon.com").not.toMatch(regex);
expect("https://x.y.z.amazon.com").toMatch(regex);
expect("https://restaurants.example.com").not.toMatch(regex);
});

test("test pattern with subdomain wildcard against malicious input", () => {
const pattern = "https://*.amazon.com";
const regex = cors.compileURLWildcards(pattern);
expect("https://restaurants.amazon.com").toMatch(regex);
expect("https://my.website/restaurants.amazon.com").not.toMatch(regex);
});
Loading