Skip to content

feat: Add Content-Security-Policy meta tag for GitHub Pages#596

Open
RichardSlater wants to merge 2 commits intomasterfrom
feature/add-content-security-policy
Open

feat: Add Content-Security-Policy meta tag for GitHub Pages#596
RichardSlater wants to merge 2 commits intomasterfrom
feature/add-content-security-policy

Conversation

@RichardSlater
Copy link
Contributor

Summary

Implements a Content-Security-Policy (CSP) via meta tag for the Ensono Stacks documentation site deployed on GitHub Pages. Since GitHub Pages does not support custom HTTP headers, this uses the <meta http-equiv="Content-Security-Policy"> approach via Docusaurus's headTags configuration.

Problem

The site was previously deployed without any Content-Security-Policy header, leaving it vulnerable to XSS attacks and other injection-based security issues.

Solution

Added a comprehensive CSP meta tag in docusaurus.config.js that:

  1. Restricts default sources to same-origin ('self')
  2. Whitelists required external domains:
    • Google Analytics (www.google-analytics.com, *.google-analytics.com, www.googletagmanager.com)
    • Algolia DocSearch (*.algolia.net, *.algolianet.com)
    • YouTube for ReactPlayer video embeds (www.youtube.com, www.youtube-nocookie.com)
  3. Blocks dangerous features:
    • object-src 'none' - Prevents Flash/plugin-based attacks
    • upgrade-insecure-requests - Forces HTTPS
  4. Disables baseUrlIssueBanner to reduce inline script sources

CSP Directives

Directive Value Reason
default-src 'self' Restrict to same-origin by default
script-src 'self' 'unsafe-inline' + GA/GTM domains Required for Docusaurus theme init and analytics
style-src 'self' 'unsafe-inline' Required for react-select's Emotion CSS-in-JS
img-src 'self' data: + GA domains Allows SVGs, data URIs, and GA pixels
font-src 'self' Fonts are self-hosted (Inter, Plus Jakarta Sans)
connect-src 'self' + GA/Algolia domains API connections for search and analytics
frame-src 'self' + YouTube ReactPlayer video embeds
form-action 'self' Form submissions restricted to same-origin
object-src 'none' Block Flash/plugins
upgrade-insecure-requests - Auto-upgrade HTTP to HTTPS

Testing Performed

Comprehensive testing was performed using Chrome DevTools MCP Server:

Pages Tested ✅

Test Page Result
Homepage / ✅ Pass - No CSP errors, all assets loaded
Documentation main /docs ✅ Pass - No CSP errors
Sidebar navigation /docs ✅ Pass - Expand/collapse works
Infrastructure docs /docs/infrastructure/azure/core_infrastructure ✅ Pass - Images and tables render
CLI MDX page /docs/stackscli/usage ✅ Pass - Screenshots display correctly
React-Select dropdown /#stacks-selector ✅ Pass - CSS-in-JS styles work
Dropdown selection /#stacks-selector ✅ Pass - Dynamic updates work
404 page /nonexistent-page ✅ Pass - Error page renders
Testing overview /docs/testing/testing_overview ✅ Pass - Complex tables render
Workloads intro /docs/workloads ✅ Pass - Content displays
Linting docs /docs/linting/eslint ✅ Pass - Admonitions render

Network Requests Verified ✅

  • www.google-analytics.com - Allowed
  • www.googletagmanager.com - Allowed
  • region1.google-analytics.com - Allowed
  • All local assets (JS, CSS, fonts, images) - Allowed

Console Errors Checked ✅

  • No CSP violation errors on any tested page
  • Google Analytics tracking requests succeed
  • All fonts, images, and scripts load correctly

Known Limitations

  1. 'unsafe-inline' required: Both script-src and style-src require 'unsafe-inline' due to:

    • Docusaurus inline scripts (theme initialization, gtag)
    • react-select's Emotion CSS-in-JS runtime styles

    Removing these would require replacing react-select with a non-CSS-in-JS component and modifying Docusaurus core.

  2. Report-Only mode not available: Content-Security-Policy-Report-Only via meta tags is not supported by browsers - only HTTP headers can use report-only mode.

  3. frame-ancestors not available: The frame-ancestors directive cannot be set via meta tags (only HTTP headers), so clickjacking protection at the frame level is not included.

Backout Plan

If issues are discovered after deployment:

  1. Revert this commit
  2. Or modify the CSP directives in docusaurus.config.js to be more permissive

Security Impact

  • ✅ Mitigates XSS attacks by restricting script/style sources
  • ✅ Prevents plugin-based attacks via object-src 'none'
  • ✅ Forces HTTPS via upgrade-insecure-requests
  • ✅ Restricts form submissions to same-origin
  • ⚠️ 'unsafe-inline' reduces protection but is required for site functionality

Checklist

  • CSP meta tag added to docusaurus.config.js
  • All required external domains whitelisted
  • Build succeeds (npm run build)
  • Comprehensive browser testing performed
  • No CSP violations on tested pages
  • Google Analytics tracking works
  • React-Select dropdown works (CSS-in-JS)
  • Documentation pages render correctly

Add CSP header via meta tag since GitHub Pages doesn't support HTTP headers.

Changes:
- Add headTags configuration with CSP meta tag
- Configure CSP directives for Google Analytics, Algolia, and YouTube
- Disable baseUrlIssueBanner to reduce inline scripts
- Use 'unsafe-inline' for script-src and style-src (required by Docusaurus
  and react-select CSS-in-JS)

CSP Directives:
- default-src: 'self'
- script-src: 'self' 'unsafe-inline' + GA/GTM domains
- style-src: 'self' 'unsafe-inline'
- img-src: 'self' data: + GA domains
- font-src: 'self' (fonts are self-hosted)
- connect-src: 'self' + GA regional endpoints + Algolia
- frame-src: 'self' + YouTube (for ReactPlayer embeds)
- form-action: 'self'
- base-uri: 'self'
- object-src: 'none'
- upgrade-insecure-requests

Security Impact:
- Mitigates XSS attacks by restricting script/style sources
- Prevents clickjacking via frame-src restrictions
- Blocks plugin-based attacks via object-src 'none'
- Forces HTTPS via upgrade-insecure-requests
Copilot AI review requested due to automatic review settings December 5, 2025 11:52
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a Content-Security-Policy (CSP) for the Ensono Stacks documentation site using a meta tag approach, since GitHub Pages doesn't support custom HTTP headers. The CSP restricts resource loading to trusted sources while allowing necessary external services like Google Analytics, Algolia search, and YouTube embeds.

Key changes:

  • Added comprehensive CSP meta tag via Docusaurus headTags configuration
  • Whitelisted required external domains (Google Analytics, Algolia, YouTube)
  • Disabled baseUrlIssueBanner to reduce inline script requirements
  • Applied code formatting fixes (quotes, trailing commas)

// This policy is enforced directly - test thoroughly before deployment
const cspDirectives = [
"default-src 'self'",
// Scripts: 'unsafe-inline' required for Docusaurus theme init, gtag, and runtime scripts
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'unsafe-inline' directive in script-src significantly weakens XSS protection. Consider using nonces or hashes for inline scripts instead. If Docusaurus requires inline scripts, document this as a known security trade-off with a plan to migrate to CSP Level 3 nonces when feasible.

Suggested change
// Scripts: 'unsafe-inline' required for Docusaurus theme init, gtag, and runtime scripts
// ⚠️ SECURITY STANDARDS VIOLATION: 'unsafe-inline' in script-src weakens XSS protection.
// See: OWASP Top 10 (A03: Injection), PCI DSS 6.6, ISO 27001 A.12.6, NIST SP 800-53 SI-10
// Docusaurus currently requires inline scripts for theme initialization and runtime. This is a known security trade-off.
// TODO: Migrate to CSP Level 3 nonces or hashes for all inline scripts. Track progress in issue #SEC-001.
// Remove 'unsafe-inline' as soon as upstream or site refactoring allows.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docusaurus doesn't support this, possibly could be mitigated by another engine (Hugo), however due to the meta based implementation if a site is compromised then it would be trivial for the attacker to modify the CSP.

// Scripts: 'unsafe-inline' required for Docusaurus theme init, gtag, and runtime scripts
"script-src 'self' 'unsafe-inline' https://www.google-analytics.com https://www.googletagmanager.com",
// Styles: 'unsafe-inline' required for react-select Emotion CSS-in-JS
"style-src 'self' 'unsafe-inline'",
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'unsafe-inline' directive in style-src reduces protection against CSS-based attacks. While react-select's Emotion CSS-in-JS may require this, consider evaluating alternative component libraries that support CSP-compliant styling or investigate if Emotion can use nonces.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something we are going to need to accept if we continue to use Docusaurus and GitHub pages.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@RichardSlater
Copy link
Contributor Author

CSP Testing Results 🧪

Comprehensive testing was performed using Chrome DevTools to verify the Content Security Policy doesn't break site functionality.

Pages Tested ✅

Page Status Notes
Homepage (/) ✅ Pass All images, fonts, and styles load correctly
Documentation (/docs) ✅ Pass Navigation and content render properly
React-Select Dropdown ✅ Pass Emotion CSS-in-JS styles work (requires unsafe-inline)
Infrastructure Docs ✅ Pass Images and diagrams load correctly
CLI Documentation ✅ Pass All pages render without issues
Testing Overview ✅ Pass Content and styling correct
Workloads Section ✅ Pass All sections accessible
404 Error Page ✅ Pass Custom error page displays correctly
ESLint Docs ✅ Pass Code blocks and documentation work

Console Verification

  • Zero CSP violations observed in Chrome DevTools console
  • All network requests complete successfully
  • Google Analytics tracking verified working with regional endpoints

CSP Directives Implemented

default-src 'self';
script-src 'self' 'unsafe-inline' https://www.google-analytics.com https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://www.google-analytics.com;
font-src 'self';
connect-src 'self' https://www.google-analytics.com https://*.google-analytics.com https://*.algolia.net https://*.algolianet.com;
frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com;
form-action 'self';
base-uri 'self';
object-src 'none';
upgrade-insecure-requests

Why unsafe-inline is Required

  1. Scripts: Docusaurus uses inline scripts for hydration and configuration
  2. Styles: react-select uses Emotion CSS-in-JS which generates inline styles dynamically

Security Notes

  • object-src 'none' blocks Flash and other plugins
  • upgrade-insecure-requests ensures HTTPS
  • base-uri 'self' prevents base tag injection attacks
  • All external domains are explicitly whitelisted (no wildcards except for regional GA endpoints)

@RichardSlater
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@RichardSlater RichardSlater enabled auto-merge (squash) December 15, 2025 11:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants