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

Add preflight handling for custom web image #332

Merged
merged 8 commits into from
Jan 16, 2024
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
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ jobs:

steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::164951611079:role/Canarytokens-staging-github-action
role-to-assume: arn:aws:iam::211125554061:role/Canarytokens-staging-github-action
role-session-name: GitHubActions-${{ github.actor }}-${{ github.workflow }}-${{ github.run_id }}-${{ github.run_number }}
aws-region: ${{ env.AWS_REGION }}
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion canarytokens/channel_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def render_GET(self, request: Request):
request.setHeader("Server", "Apache")
return resp

def render_OPTIONS(self, request):
def render_OPTIONS(self, request: Request):
"""
Alert as if it is a normal GET request, but return the expected content and headers.
"""
Expand Down
29 changes: 26 additions & 3 deletions canarytokens/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,9 +637,7 @@ def _get_response_for_web_image(
# render template
return template.render(**fortune_template_params).encode()

# If a browser makes cross-origin requests for this img that aren't "simple" it will reject
# the image response without this Canarytokens server permitting all cross-origin requests
request.setHeader("Access-Control-Allow-Origin", "*")
_check_and_add_cors_headers(request)

if canarydrop.web_image_enabled and canarydrop.web_image_path.exists():
# set response mimetype
Expand Down Expand Up @@ -753,3 +751,28 @@ def create_token_hit(
AnyTokenHit,
hit_info,
)


def _check_and_add_cors_headers(request: Request):
"""
According to https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request, we
should check for `Access-Control-Request-Method` and `Origin` and optionally,
`Access-Control-Request-Headers` headers in an OPTIONS request to determine its a preflight request; and
respond with `Access-Control-Allow-Origin` and `Access-Control-Allow-Methods`. Else, we
will add `Access-Control-Allow-Origin: *` to the GET request.
"""
if request.method.upper() == b"GET":
request.setHeader("Access-Control-Allow-Origin", "*")
elif request.method.upper() == b"OPTIONS":
if (
request.getHeader("Access-Control-Request-Method") is None
or request.getHeader("Origin") is None
):
return

acr_headers = request.getHeader("Access-Control-Request-Headers")
if acr_headers is not None:
request.setHeader("Access-Control-Allow-Headers", acr_headers)

request.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"))
request.setHeader("Access-Control-Allow-Methods", "OPTIONS, GET, POST")
6 changes: 3 additions & 3 deletions frontend/frontend.env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ CANARY_DOMAINS=127.0.0.1
CANARY_NXDOMAINS=nx.127.0.0.1
#CANARY_SWITCHBOARD_SETTINGS_PATH=

CANARY_SENTRY_DSN="https://06a5bffddffd4d0f8a9b675ea82a5fb5@o1177763.ingest.sentry.io/6312922"
CANARY_SENTRY_ENVIRONMENT=local
CANARY_SENTRY_ENABLE=False
#CANARY_SENTRY_DSN=
#CANARY_SENTRY_ENVIRONMENT=local
#CANARY_SENTRY_ENABLE=False

# template configurations
#CANARY_TEMPLATES_PATH=
Expand Down
135 changes: 134 additions & 1 deletion tests/integration/test_custom_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,6 @@ def test_custom_image_web_image(
webhook_receiver,
runv2,
runv3,
# clean_uploads_dir
):
run_or_skip(version, runv2=runv2, runv3=runv3)

Expand Down Expand Up @@ -298,3 +297,137 @@ def test_custom_image_web_image(
assert token_hit.geo_info.ip == requests.get("https://ipinfo.io/ip").text
else:
assert token_hit.geo_info.ip == "127.0.0.1"


@pytest.mark.parametrize("version", [v3])
@pytest.mark.parametrize(
"request_details, resp_details",
[
pytest.param(
{"method": "GET", "headers": {}},
{"headers": {"Access-Control-Allow-Origin": "*"}, "not_headers": []},
id="Get-Request-Cors-Support",
),
pytest.param(
{
"method": "OPTIONS",
"headers": {
"Access-Control-Request-Method": "GET",
"Origin": "test.com",
},
},
{
"headers": {
"Access-Control-Allow-Methods": "OPTIONS, GET, POST",
"Access-Control-Allow-Origin": "test.com",
},
"not_headers": ["Access-Control-Request-Method"],
},
id="Preflight-Cors-Support",
),
pytest.param(
{
"method": "OPTIONS",
"headers": {
"Origin": "test.com",
},
},
{
"headers": {},
"not_headers": [
"Access-Control-Allow-Methods",
"Access-Control-Allow-Origin",
],
},
id="Bad-Preflight-Cors-Request",
),
pytest.param(
{
"method": "GET",
"headers": {
"Access-Control-Request-Method": "GET",
"Origin": "test.com",
},
},
{
"headers": {
"Access-Control-Allow-Origin": "*",
},
"not_headers": [
"Access-Control-Request-Method",
"Access-Control-Allow-Methods",
],
},
id="Get-with-Preflight-Cors-Headers",
),
],
)
def test_custom_image_web_image_cors_support(
version, webhook_receiver, runv2, runv3, request_details, resp_details
):
run_or_skip(version, runv2=runv2, runv3=runv3)
file_name = "canary_image.png"
file_mimetype = "image/{mimetype}".format(
mimetype=file_name[-3:].replace("jpg", "jpeg")
)
with open("data/{file}".format(file=file_name), "rb") as fp:
# record contents
input_file = fp.read()

# create SpooledTemporaryFile
temp_file = SpooledTemporaryFile()
temp_file.write(input_file)
temp_file.seek(0)
# initialize request
web_image = UploadedImage(
filename=file_name, content_type=file_mimetype, file=temp_file
)
memo = "custom web token memo!"
token_request = CustomImageTokenRequest(
token_type=TokenTypes.WEB_IMAGE,
web_image=web_image,
webhook_url=webhook_receiver,
memo=Memo(memo),
)

# Create custom image token
resp = create_token(token_request=token_request, version=version)
token_info = CustomImageTokenResponse(**resp)

# ensure web_image is enabled
_res = set_token_settings(
setting=WebImageSettingsRequest(
value="on",
token=token_info.token,
auth=token_info.auth_token,
),
version=version,
)
# check success of the web_image settings update
if isinstance(version, V2):
assert _res["result"] == "success"
elif isinstance(version, V3):
assert _res["message"] == "success"

# Trigger the token
trigger_headers = {
"Referrer-Policy": "strict-origin-when-cross-origin",
"Accept": "{mimetype}".format(
mimetype=file_mimetype,
),
}
trigger_headers.update(request_details["headers"])

_resp = trigger_http_token(
token_info=token_info,
version=version,
headers=trigger_headers,
stream=True,
method=request_details["method"],
)
for header, value in resp_details["headers"].items():
assert header in _resp.headers
assert value in _resp.headers[header]

for header in resp_details["not_headers"]:
assert header not in _resp.headers
5 changes: 4 additions & 1 deletion tests/units/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from canarytokens.utils import coerce_to_float, get_deployed_commit_sha
from canarytokens.utils import (
coerce_to_float,
get_deployed_commit_sha,
)


def test_get_deployed_commit_sha(tmpdir):
Expand Down
Loading