Hello,
I have created a Lambda@Edge function in Python3.9 which are suppose to add CSP headers with nounce together with CloudFront.
What I am doing is that I read the index.html from my S3 bucket, modifies the <script> elements and add my nounce and then return the modified index.html as content.
It works great with the exception that it also opens javascript files and style-files as well and insert the index.html content into these, breaking them. This is because my code reads the index.html and then send it back as the body, of course.
Is my implementation odd? Or do I simply need to somehow in my python code only apply the content based on X,Y,Z?
Here is the function association on my CloudFront Behaviour

My Lambda function
import boto3
import secrets
import os
from botocore.exceptions import ClientError
s3_client = boto3.client('s3', region_name=os.environ.get('AWS_REGION'))
# The S3 bucket where the page and index.html resist
s3_bucket = "foobar.app"
# This should always be foobar.app since we whitelist *.{base_uri} in the CSP
base_uri = "foobar.app"
# Cache the index so we only refetch it when the ETag changes.
index_cache = {
"contents": None,
"etag": None,
}
def lambda_handler(event, context):
contents = fetch_index()
csp, nonce = generate_csp()
contents = contents.replace("<script", f'<script nonce="{nonce}"')
return {
"status": 200,
"statusDescription": "OK",
"body": contents,
"headers": {
"content-security-policy": [
{
"key": "Content-Security-Policy",
"value": csp
}
]
}
}
def fetch_index():
global index_cache
s3_object = None
index = None
try:
# Get the object from S3 with conditional GET using ETag
response = s3_client.get_object(
Bucket=s3_bucket,
Key="index.html"
)
index = response["Body"].read().decode("utf-8")
except ClientError as ex:
# If the status code is 304 (Not Modified), return the cached contents
if ex.response["Error"]["Code"] == "NotModified":
return index_cache["contents"]
else:
raise ex
index_cache["contents"] = index
index_cache["etag"] = response["ETag"]
return index
def generate_csp():
nonce = secrets.token_urlsafe(16)
csp = (
f"default-src 'self' https://*.{base_uri}; "
f"script-src 'self' https://*.{base_uri} 'nonce-{nonce}' 'strict-dynamic'; "
f"style-src 'self' 'unsafe-inline' https://*.{base_uri}; "
f"img-src 'self' https://*.{base_uri} data: blob: https:; "
f"font-src 'self' data: https:; "
f"connect-src 'self' https://*.{base_uri} wss://*.{base_uri}; "
f"frame-src 'self' https://*.{base_uri}; "
f"object-src 'none'; "
f"base-uri 'self'; "
f"form-action 'self'; "
f"worker-src 'self' blob:; "
)
return csp, nonce
This will not work with a nonce unfortunately as it needs to be uniquely generated per page visit. That is why I am using a Lambda to generate and inject the nonce into the HTML code and the response CSP header.
You can read more here https://content-security-policy.com/nonce/