Skip to content

Commit

Permalink
fix: Use response headers instead
Browse files Browse the repository at this point in the history
  • Loading branch information
jackylamhk committed Sep 7, 2024
1 parent 5853048 commit a5e241c
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 61 deletions.
21 changes: 16 additions & 5 deletions docs/single-page-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,18 +224,29 @@ constructs:

The first domain in the list will be considered the main domain. In this case, `mywebsite.com` will redirect to `www.mywebsite.com`.

### Allow iframes
### Response headers policy

By default, as recommended [for security reasons](https://scotthelme.co.uk/hardening-your-http-response-headers/#x-frame-options), the single page application cannot be embedded in an iframe.
By default, the response headers policy is set to [Managed-SecurityHeadersPolicy](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-response-headers-policies.html#managed-response-headers-policies-security). To override this policy, set the `security.responseHeadersPolicy` to the policy ID:

To allow embedding the website in an iframe, set it up explicitly:
```yaml
constructs:
landing:
# ...
responseHeadersPolicy: <Policy ID>
```

or create a [response headers policy](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.ResponseHeadersPolicyProps.html) inline:

```yaml
constructs:
landing:
# ...
security:
allowIframe: true
responseHeadersPolicy:
responseHeadersPolicyName: CustomPolicy
securityHeadersBehavior:
contentSecurityPolicy:
contentSecurityPolicy: default-src https:;
override: true
```

## Extensions
Expand Down
21 changes: 16 additions & 5 deletions docs/static-website.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,18 +227,29 @@ When a browser requests the URL of a non-existing file, the `error.html` file wi

Do not use this setting when doing JavaScript URL routing: this will break URL routing.

### Allow iframes
### Response headers policy

By default, as recommended [for security reasons](https://scotthelme.co.uk/hardening-your-http-response-headers/#x-frame-options), the static website cannot be embedded in an iframe.
By default, the response headers policy is set to [Managed-SecurityHeadersPolicy](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-response-headers-policies.html#managed-response-headers-policies-security). To override this policy, set the `security.responseHeadersPolicy` to the policy ID:

To allow embedding the website in an iframe, set it up explicitly:
```yaml
constructs:
landing:
# ...
responseHeadersPolicy: <Policy ID>
```

or create a [response headers policy](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.ResponseHeadersPolicyProps.html) inline:

```yaml
constructs:
landing:
# ...
security:
allowIframe: true
responseHeadersPolicy:
responseHeadersPolicyName: CustomPolicy
securityHeadersBehavior:
contentSecurityPolicy:
contentSecurityPolicy: default-src https:;
override: true
```

## Extensions
Expand Down
69 changes: 21 additions & 48 deletions src/constructs/aws/abstracts/StaticWebsiteAbstract.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import * as acm from "aws-cdk-lib/aws-certificatemanager";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import type { CfnDistribution, ErrorResponse } from "aws-cdk-lib/aws-cloudfront";
import {
AllowedMethods,
CachePolicy,
Distribution,
FunctionEventType,
HttpVersion,
ResponseHeadersPolicy,
ViewerProtocolPolicy,
} from "aws-cdk-lib/aws-cloudfront";
import { S3Origin } from "aws-cdk-lib/aws-cloudfront-origins";
import type { IResponseHeadersPolicy } from "aws-cdk-lib/aws-cloudfront";
import type { BucketProps, CfnBucket } from "aws-cdk-lib/aws-s3";
import { Bucket } from "aws-cdk-lib/aws-s3";
import type { Construct as CdkConstruct } from "constructs";
Expand All @@ -26,7 +26,6 @@ import { emptyBucket, invalidateCloudFrontCache } from "../../../classes/aws";
import ServerlessError from "../../../utils/error";
import type { Progress } from "../../../utils/logger";
import { getUtils } from "../../../utils/logger";
import { ensureNameMaxLength } from "../../../utils/naming";
import { s3Sync } from "../../../utils/s3-sync";

export const COMMON_STATIC_WEBSITE_DEFINITION = {
Expand All @@ -43,12 +42,8 @@ export const COMMON_STATIC_WEBSITE_DEFINITION = {
],
},
certificate: { type: "string" },
security: {
type: "object",
properties: {
allowIframe: { type: "boolean" },
},
additionalProperties: false,
responseHeadersPolicy: {
anyOf: [{ type: "string" }, { type: "object" }],
},
errorPage: { type: "string" },
redirectToMainDomain: { type: "boolean" },
Expand Down Expand Up @@ -107,13 +102,22 @@ export abstract class StaticWebsiteAbstract extends AwsConstruct {
);
}

const functionAssociations = [
{
function: this.createResponseFunction(),
eventType: FunctionEventType.VIEWER_RESPONSE,
},
];

let responseHeadersPolicy: IResponseHeadersPolicy;
if (typeof this.configuration.responseHeadersPolicy === "string") {
responseHeadersPolicy = ResponseHeadersPolicy.fromResponseHeadersPolicyId(
scope,
"ResponseHeadersPolicy",
this.configuration.responseHeadersPolicy
);
} else if (this.configuration.responseHeadersPolicy !== undefined) {
responseHeadersPolicy = new ResponseHeadersPolicy(
scope,
"ResponseHeadersPolicy",
this.configuration.responseHeadersPolicy
);
} else {
responseHeadersPolicy = ResponseHeadersPolicy.SECURITY_HEADERS;
}
this.distribution = new Distribution(this, "CDN", {
comment: `${provider.stackName} ${id} website CDN`,
// Send all page requests to index.html
Expand All @@ -126,7 +130,7 @@ export abstract class StaticWebsiteAbstract extends AwsConstruct {
// See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html#managed-cache-policies-list
cachePolicy: CachePolicy.CACHING_OPTIMIZED,
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
functionAssociations: functionAssociations,
responseHeadersPolicy,
},
errorResponses: this.errorResponses(),
// Enable http2 transfer for better performances
Expand Down Expand Up @@ -329,37 +333,6 @@ export abstract class StaticWebsiteAbstract extends AwsConstruct {
return [defaultResponse(403), defaultResponse(404)];
}

private createResponseFunction(): cloudfront.Function {
const securityHeaders: Record<string, { value: string }> = {
"x-frame-options": { value: "SAMEORIGIN" },
"x-content-type-options": { value: "nosniff" },
"x-xss-protection": { value: "1; mode=block" },
"strict-transport-security": { value: "max-age=63072000" },
};
if (this.configuration.security?.allowIframe === true) {
delete securityHeaders["x-frame-options"];
}
const jsonHeaders = JSON.stringify(securityHeaders, undefined, 4);
/**
* CloudFront function that manipulates the HTTP responses to add security headers.
*/
const code = `function handler(event) {
var response = event.response;
response.headers = Object.assign({}, ${jsonHeaders}, response.headers);
return response;
}`;

const functionName = ensureNameMaxLength(
`${this.provider.stackName}-${this.provider.region}-${this.id}-response`,
64
);

return new cloudfront.Function(this, "ResponseFunction", {
functionName,
code: cloudfront.FunctionCode.fromInline(code),
});
}

getBucketProps(): BucketProps {
return {
// For a static website, the content is code that should be versioned elsewhere
Expand Down
8 changes: 5 additions & 3 deletions src/utils/getDefaultCfnFunctionAssociations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export function getCfnFunctionAssociations(distribution: CfnDistribution): CfnFu
const defaultBehavior = (distribution.distributionConfig as CfnDistribution.DistributionConfigProperty)
.defaultCacheBehavior as CfnDistribution.DefaultCacheBehaviorProperty;

return (defaultBehavior.functionAssociations as Array<CfnDistribution.FunctionAssociationProperty>).map(
cdkFunctionAssociationToCfnFunctionAssociation
);
return defaultBehavior.functionAssociations instanceof Array
? (defaultBehavior.functionAssociations as Array<CfnDistribution.FunctionAssociationProperty>).map(
cdkFunctionAssociationToCfnFunctionAssociation
)
: [];
}

0 comments on commit a5e241c

Please sign in to comment.