Skip to content

Lamda@Edge for adding CSP headers to CloudFront

0

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 Associations

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

asked a year ago515 views

1 Answer
0

If the intent is to add CSP and other headers, you can create a custom CloudFront response policy as per How do I add HTTP security headers to CloudFront responses?

Screenshot below for illustration Enter image description here

AWS
EXPERT

answered a year ago

  • 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/

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.