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

Provide helpful terminal messages to fix web component CSP errors #724

Merged
merged 1 commit into from
Aug 4, 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
61 changes: 60 additions & 1 deletion mesop/server/static_file_serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
from collections import OrderedDict
from io import BytesIO
from typing import Any, Callable
from urllib.parse import urlparse

from flask import Flask, Response, g, request, send_file
from flask import Flask, Response, g, make_response, request, send_file
from werkzeug.security import safe_join

from mesop.exceptions import MesopException
from mesop.runtime import runtime
from mesop.server.constants import WEB_COMPONENTS_PATH_SEGMENT
from mesop.utils import terminal_colors as tc
from mesop.utils.runfiles import get_runfile_location, has_runfiles
from mesop.utils.url_utils import sanitize_url_for_csp

Expand Down Expand Up @@ -112,6 +114,62 @@ def serve_file(path: str):
else:
return send_file(retrieve_index_html(), download_name="index.html")

@app.route("/__csp__", methods=["POST"])
def csp_report():
# Get the CSP violation report from the request
# Flask expects the MIME type to be application/json
# but it's actually application/csp-report
report = request.get_json(force=True)

document_uri: str = report["csp-report"]["document-uri"]
path = urlparse(document_uri).path
blocked_uri: str = report["csp-report"]["blocked-uri"]
# Remove the path from blocked_uri, keeping only the origin.
blocked_site = (
urlparse(blocked_uri).scheme + "://" + urlparse(blocked_uri).netloc
)
violated_directive: str = report["csp-report"]["violated-directive"]
if violated_directive == "script-src-elem":
keyword_arg = "allowed_script_srcs"
elif violated_directive == "connect-src":
keyword_arg = "allowed_connect_srcs"
elif violated_directive == "frame-ancestors":
keyword_arg = "allowed_iframe_parents"
elif violated_directive == "require-trusted-types-for":
keyword_arg = "dangerously_disable_trusted_types"
else:
raise Exception("Unexpected CSP violation:", violated_directive, report)
keyword_arg_value = f"""[
'{tc.CYAN}{blocked_site}{tc.RESET}',
]"""
if keyword_arg == "dangerously_disable_trusted_types":
keyword_arg_value = f"{tc.CYAN}True{tc.RESET}"
print(
f"""
{tc.RED}⚠️ Content Security Policy Error ⚠️{tc.RESET}
{tc.YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{tc.RESET}

{tc.CYAN}Directive:{tc.RESET} {tc.GREEN}{violated_directive}{tc.RESET}
{tc.CYAN}Blocked URL:{tc.RESET} {tc.GREEN}{blocked_uri}{tc.RESET}
{tc.CYAN}App path:{tc.RESET} {tc.GREEN}{path}{tc.RESET}

{tc.YELLOW}ℹ️ If this is coming from your web component,
update your security policy like this:{tc.RESET}

{tc.MAGENTA}@me.page({tc.RESET}
{tc.BLUE}security_policy={tc.RESET}{tc.MAGENTA}me.SecurityPolicy({tc.RESET}
{tc.GREEN}{keyword_arg}={tc.RESET}{keyword_arg_value}
{tc.MAGENTA}){tc.RESET}
{tc.MAGENTA}){tc.RESET}

{tc.YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{tc.RESET}
""" # noqa: RUF001
)

response = make_response()
response.status_code = 204
return response

@app.before_request
def generate_nonce():
g.csp_nonce = secrets.token_urlsafe(16)
Expand Down Expand Up @@ -150,6 +208,7 @@ def add_security_headers(response: Response):
# https://angular.io/guide/security#enforcing-trusted-types
"trusted-types": "angular angular#unsafe-bypass lit-html",
"require-trusted-types-for": "'script'",
"report-uri": "/__csp__",
}
)
if page_config and page_config.stylesheets:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ style-src 'self' 'unsafe-inline' fonts.googleapis.com
script-src 'self' 'nonce-{{NONCE}}'
trusted-types angular angular#unsafe-bypass lit-html
require-trusted-types-for 'script'
report-uri /__csp__
frame-ancestors 'self' google.com
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ style-src 'self' 'unsafe-inline' fonts.googleapis.com http://google.com/styleshe
script-src 'self' 'nonce-{{NONCE}}' http://google.com/allowed_script_srcs/1%2C1%3B2 http://google.com/allowed_script_srcs/2%2C1%3B2
trusted-types angular angular#unsafe-bypass lit-html
require-trusted-types-for 'script'
report-uri /__csp__
connect-src 'self' http://google.com/allowed_connect_srcs/1%2C1%3B2 http://google.com/allowed_connect_srcs/2%2C1%3B2
frame-ancestors 'self' http://google.com/allowed_iframe_parents/1%2C1%3B2 http://google.com/allowed_iframe_parents/2%2C1%3B2
1 change: 1 addition & 0 deletions mesop/tests/e2e/snapshots/web_security_test.ts_csp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ style-src 'self' 'unsafe-inline' fonts.googleapis.com
script-src 'self' 'nonce-{{NONCE}}'
trusted-types angular angular#unsafe-bypass lit-html
require-trusted-types-for 'script'
report-uri /__csp__
frame-ancestors 'self' https://google.github.io
7 changes: 7 additions & 0 deletions mesop/utils/terminal_colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
RESET = "\033[0m"
Loading