Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple Allowed Origins in CORS #1006

Closed
straygar opened this issue Feb 5, 2022 · 13 comments · Fixed by #2279
Closed

Support multiple Allowed Origins in CORS #1006

straygar opened this issue Feb 5, 2022 · 13 comments · Fixed by #2279
Assignees
Labels
event_handlers feature-request feature request help wanted Could use a second pair of eyes/hands

Comments

@straygar
Copy link

straygar commented Feb 5, 2022

Is your feature request related to a problem? Please describe.
In some cases, we would like to support multiple origins in CORS when defining an API Gateway/ALB event handler (e.g. support calling an API from "localhost" in dev, or webapps living in different origins calling the same API).

Describe the solution you'd like
Allow providing a list of origins CORSConfig(allow_origins=[]) (source). If the client's Origin is in that list, that value is returned in the response header. Otherwise - an arbitrary (e.g. 1st) value from the list can be returned.

@straygar straygar added feature-request feature request triage Pending triage from maintainers labels Feb 5, 2022
@boring-cyborg
Copy link

boring-cyborg bot commented Feb 5, 2022

Thanks for opening your first issue here! We'll come back to you as soon as we can.

@michaelbrewer
Copy link
Contributor

This will rely on using the origin to determine which one of the possible allow_origins to use. So a little thought will be needed in how to implement this, this article covers this fairly well: https://crashtest-security.com/multiple-values-access-control-allow-origin/

FYI @heitorlessa

@heitorlessa
Copy link
Contributor

heitorlessa commented Feb 7, 2022

Hey @straygar thanks a lot for raising this idea with us!

I'd be careful on relying on Origin to make that decision for two reasons: 1/ It's an attack vector as this can be easily spoofed, and 2/ API Gateway doesn't support multiple origins by default, which means you need to create a OPTIONS method for each path of your API definition pointing to your Lambda function handling it.

As 2 can become tedious and error-prone, it opens the door for these sort of attacks that are quite common: https://we45.com/blog/3-ways-to-exploit-cors-misconfiguration

I'd like to wait for more customers demand before we look into it, so we can think of additional factors besides Origin and make a good documentation to reduce chances of misconfiguration.


Update after @michaelbrewer reach out to prevent ambiguity

The safest alternative is to use multiple APIs however painful this is. If creating multiple APIs isn't an option, the second safest option is below.

  1. Configure API Gateway to forward pre-flight requests to Lambda. You must configure OPTIONS method on all resources pointing to your Lambda function.
  2. Handle pre-flight in your Lambda with care. Use a function annotated with @app.route(rule=".*", method="OPTIONS") and set app._cors.allow_origin using one of the trusted static origins you might keep in a list - do not use dynamic values (e.g. regex) to prevent abuse on Origin header spoofing.

Sample annotated function to handle pre-flight CORS so you don't have to update every other routing function.

from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, CORSConfig
from aws_lambda_powertools.event_handler.exceptions import UnauthorizedError

# Default CORS for a prod environment
trusted_origins = ["<your static list>"]
cors_config = CORSConfig(allow_origin="https://prod.my.domain", max_age=100) # NOTE: never '*'

app = ApiGatewayResolver(cors=cors_config)


@app.route(method="OPTIONS", rule=".*") # Matches any pre-flight request coming from API Gateway
def preflight_handler():
    """Handles multi-origin preflight requests"""
    origin = app.current_event.get_header_value(name="Origin", default_value="")
    if origin in trusted_origins:
        app._cors.allow_origin = origin
    raise UnauthorizedError("Nope")  # NOTE: do not return a default header to prevent abuse

@heitorlessa heitorlessa added need-customer-feedback Requires more customers feedback before making or revisiting a decision and removed triage Pending triage from maintainers labels Feb 7, 2022
@michaelbrewer
Copy link
Contributor

Optimal CORS configuration is always tricky. Classical solutions are a plenty via nginx configurations at allow for a set of regex origins.

Not sure why there is a gap here for API GW. But it sounds like spinning up multiple endpoints is what is recommended.

Better to be safe.

@michaelbrewer
Copy link
Contributor

@heitorlessa this is what the linked article does. I am just saying we could also do it in behave of the client.

@michaelbrewer
Copy link
Contributor

@heitorlessa NOTE using _cors may result in breaking changes in the future. Also not sure what the exploit would be of return the main allow_origin as the default over UnauthorizedError. I am not sure what the abuse would be?

@michaelbrewer
Copy link
Contributor

michaelbrewer commented Feb 7, 2022

Just for comment i put up a PR:

from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, CORSConfig
from aws_lambda_powertools.event_handler.exceptions import UnauthorizedError

# allow_origin - default origin and fall through origin
# allow_origins - additional trusted origins
cors_config = CORSConfig(allow_origin="https://stg1.my.domain/", allow_origins=["https://stg2.my.domain/"])
app = ApiGatewayResolver(cors=cors_config)

print(app({"path": "/another-one", "httpMethod": "GET", "headers": {}}, None))
# {'statusCode': 404, 'headers': {'Access-Control-Allow-Origin': 'https://stg1.my.domain/', 'Access-Control-Allow-Headers': 'Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key', 'Content-Type': 'application/json'}, 'body': '{"statusCode":404,"message":"Not found"}', 'isBase64Encoded': False}

print(app({"path": "/another-one", "httpMethod": "GET", "headers": {"Origin": "https://stg2.my.domain/"}}, None))
#  {'statusCode': 404, 'headers': {'Access-Control-Allow-Origin': 'https://stg2.my.domain/', 'Access-Control-Allow-Headers': 'Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key', 'Content-Type': 'application/json'}, 'body': '{"statusCode":404,"message":"Not found"}', 'isBase64Encoded': False}

@heitorlessa heitorlessa added the triage Pending triage from maintainers label Aug 22, 2022
@leandrodamascena leandrodamascena added event_handlers and removed triage Pending triage from maintainers need-customer-feedback Requires more customers feedback before making or revisiting a decision labels Feb 16, 2023
@leandrodamascena
Copy link
Contributor

Moving this issue from Ideas to Backlog to start working on it soon.

@heitorlessa heitorlessa added the help wanted Could use a second pair of eyes/hands label Mar 10, 2023
@rubenfonseca
Copy link
Contributor

Note: Function URLs already support multiple origins:

image

@rubenfonseca
Copy link
Contributor

Note: API Gateway HTTP (v2) already supports multiple origins:

image

@rubenfonseca
Copy link
Contributor

rubenfonseca commented Apr 27, 2023

My current understanding of the situation is:

API Gateway REST API Gateway HTTP ALB Function URL
CORS supported natively?
Multiple Origins supported?
Can use custom Preflight Lambda?

So it's a little bit of messy situation. We can of course add support to multiple origins when using a custom Lambda Preflight, but it can also be hard to set it up correctly.

If we're going to move forward, I suggest we keep the existing API and add a new extra_origins: Optional[List[str]] parameter.

Then we can add a new issue for Powertools V3 where we combine both arguments into a allowed_origins: str | List[str].

@heitorlessa thoughts?

Security

Supporting multiple origins should not cause any additional security concerns. Those mainly come from using dynamic origins (e.g: regexes). We should also not leak information by returning an origin when one is not passed -- CORS backend should just accept or reject a requested origin.

@github-actions
Copy link
Contributor

⚠️COMMENT VISIBILITY WARNING⚠️

This issue is now closed. Please be mindful that future comments are hard for our team to see.

If you need more assistance, please either tag a team member or open a new issue that references this one.

If you wish to keep having a conversation with other community members under this issue feel free to do so.

@github-actions github-actions bot added the pending-release Fix or implementation already in dev waiting to be released label May 18, 2023
@github-actions
Copy link
Contributor

github-actions bot commented Jun 2, 2023

This is now released under 2.16.1 version!

@github-actions github-actions bot removed the pending-release Fix or implementation already in dev waiting to be released label Jun 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
event_handlers feature-request feature request help wanted Could use a second pair of eyes/hands
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants