Welcome to Chapter 17! In the realm of web development, security is paramount, and while Static Site Generators (SSGs) inherently offer a higher baseline of security compared to dynamic applications, they are not entirely immune to vulnerabilities. The static nature of SSGs reduces the attack surface by eliminating server-side databases, complex application logic, and direct user input processing, but client-side risks and build-process vulnerabilities still exist.

In this chapter, we will enhance our Rust SSG by integrating crucial security considerations. Our focus will be on protecting against common client-side attacks like Cross-Site Scripting (XSS) through robust content sanitization, implementing Content Security Policy (CSP) to mitigate a wide range of injection attacks, and discussing best practices for secure dependency management and deployment. By the end of this chapter, your SSG will not only generate high-performance sites but also produce outputs that adhere to modern security standards, giving you confidence in deploying your content to production.

This chapter assumes you have a working build pipeline, content parsing, and HTML rendering in place from previous chapters. We will modify the content processing and output generation stages to inject security features. Our expected outcome is an SSG that automatically sanitizes user-provided content and can generate a configurable CSP, alongside a clear understanding of broader security practices for SSG projects.

Planning & Design

Even for static sites, a layered security approach is crucial. We need to consider security at various stages: from the input content, through the build process, to the final deployed output. Our design will focus on integrating security features directly into the SSG’s pipeline.

Security Integration Flow

The following diagram illustrates where and how security considerations will be integrated into our SSG’s build and deployment process:

graph TD A["Content Authoring"] --> B{"Content Input (Markdown, HTML)"}; B --> C{"Parse Frontmatter & Markdown"}; C -->|Unsanitized Markdown AST| D["Markdown AST Transformation"]; D -->|Post-processing| E["Component Rendering & Hydration"]; E --> F{"HTML Output Generation"}; F -->|Raw HTML| G["HTML Sanitization (XSS Prevention)"]; G --> H["Apply Content Security Policy (CSP) Meta Tag"]; H --> I["Final Static HTML Files"]; subgraph Build_Process["SSG Build Process Security"] C --> J["Dependency Scanning (cargo audit)"]; J --> K{"Dependencies Secure?"}; K -->|Yes| D; K -->|No| L["Alert & Fix Vulnerabilities"]; L --> J; end subgraph Deployment_Considerations["Deployment Security"] I --> M["CDN / Hosting Deployment"]; M --> N["Secure File Permissions"]; N --> O["HTTP Headers & TLS Configuration"]; O --> P["Monitoring & Logging"]; end style G fill:#d0f0c0,stroke:#333,stroke-width:2px; style H fill:#d0f0c0,stroke:#333,stroke-width:2px; style J fill:#d0f0c0,stroke:#333,stroke-width:2px; style K fill:#d0f0c0,stroke:#333,stroke-width:2px; style L fill:#f00,stroke:#333,stroke-width:2px; style N fill:#d0f0c0,stroke:#333,stroke-width:2px; style O fill:#d0f0c0,stroke:#333,stroke-width:2px; style P fill:#d0f0c0,stroke:#333,stroke-width:2px;

Key Security Steps:

  1. HTML Sanitization (XSS Prevention): After converting Markdown to HTML but before final output, we’ll sanitize the generated HTML to remove potentially malicious scripts or attributes. This is crucial if your content authors can include raw HTML or if your Markdown parser allows embedding of potentially unsafe elements.
  2. Content Security Policy (CSP) Generation: We’ll add logic to our SSG to generate a <meta http-equiv="Content-Security-Policy"> tag within the <head> of our output HTML files. This policy will restrict what resources (scripts, styles, images, fonts) the browser is allowed to load and execute, significantly reducing the impact of XSS and data injection attacks.
  3. Dependency Scanning: While not directly part of the SSG’s runtime, integrating tools like cargo audit into our CI/CD pipeline is critical for ensuring that our SSG itself (and its dependencies) are free from known vulnerabilities.
  4. Deployment Best Practices: We’ll discuss configuring secure file permissions, HTTP headers, TLS, and monitoring during deployment.

File Structure Changes

We will introduce a new module for security-related configurations and helpers, and modify existing modules for content processing and configuration.

  • src/config.rs: Add fields for CSP directives.
  • src/security.rs: New module to handle HTML sanitization and CSP generation logic.
  • src/content_processor.rs (or similar, where Markdown is converted to HTML): Integrate the sanitization step.
  • src/renderer.rs (or similar, where HTML is assembled): Integrate CSP meta tag.

Step-by-Step Implementation

a) Setup/Configuration

First, we need to add a dependency for HTML sanitization. A popular and robust Rust crate for this is ammonia. We’ll also update our Config structure to allow for CSP customization.

1. Add ammonia dependency:

Open Cargo.toml and add ammonia to your [dependencies] section:

# Cargo.toml

[dependencies]
# ... existing dependencies ...
ammonia = "0.7" # For HTML sanitization
serde = { version = "1.0", features = ["derive"] } # Ensure this is present
toml = "0.8" # Or "0.7" depending on your version
yaml-frontmatter = "0.5" # Or similar for frontmatter
tera = "1.19" # Or your templating engine
pulldown-cmark = "0.10" # Or your markdown parser
log = "0.4"
env_logger = "0.11"
# ... other dependencies ...

Why ammonia? ammonia is a powerful and secure HTML sanitization library written in Rust. It’s built on html5ever, which provides robust HTML parsing, ensuring it handles even malformed HTML gracefully and securely removes dangerous elements like <script> tags, on* attributes, and other potential XSS vectors.

2. Update Config for CSP:

We need a way for users to configure their Content Security Policy. We’ll add a csp field to our Config struct in src/config.rs. This will allow the user to define directives in the config.toml file.

// src/config.rs
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Config {
    pub base_url: String,
    pub site_name: String,
    pub content_dir: PathBuf,
    pub output_dir: PathBuf,
    pub templates_dir: PathBuf,
    pub static_dir: Option<PathBuf>,
    pub default_template: String,
    pub pagination: Option<PaginationConfig>,
    pub taxonomies: Option<HashMap<String, TaxonomyConfig>>,
    pub build_parallel_jobs: Option<usize>,
    pub enable_incremental_build: Option<bool>,
    
    // New: Content Security Policy configuration
    #[serde(default)] // Allows this field to be omitted in config.toml
    pub csp: CspConfig, 
}

#[derive(Debug, Deserialize, Serialize, Clone, Default)] // Default trait for CspConfig
pub struct CspConfig {
    #[serde(default = "default_csp_enabled")]
    pub enabled: bool,
    #[serde(default = "default_csp_directives")]
    pub directives: HashMap<String, String>,
}

fn default_csp_enabled() -> bool {
    true // CSP is enabled by default
}

fn default_csp_directives() -> HashMap<String, String> {
    let mut map = HashMap::new();
    map.insert("default-src".to_string(), "'self'".to_string());
    map.insert("script-src".to_string(), "'self' 'unsafe-inline'".to_string()); // 'unsafe-inline' is often needed for hydration scripts, but should be avoided if possible. We'll refine this.
    map.insert("style-src".to_string(), "'self' 'unsafe-inline'".to_string()); // 'unsafe-inline' for inline styles, refine later.
    map.insert("img-src".to_string(), "'self' data:".to_string());
    map.insert("font-src".to_string(), "'self'".to_string());
    map.insert("connect-src".to_string(), "'self'".to_string());
    map.insert("object-src".to_string(), "'none'".to_string());
    map.insert("base-uri".to_string(), "'self'".to_string());
    map.insert("form-action".to_string(), "'self'".to_string());
    map
}

// ... existing structs like PaginationConfig, TaxonomyConfig ...

impl Config {
    pub fn load(path: &Path) -> Result<Self, Box<dyn Error>> {
        // ... existing implementation ...
        let config_string = fs::read_to_string(path)?;
        let config: Config = toml::from_str(&config_string)?;
        Ok(config)
    }
}

Why #[serde(default)] and Default trait? By using #[serde(default)] on the csp field of Config and implementing Default for CspConfig, we ensure that if a user doesn’t specify [csp] in their config.toml, a default, secure CSP configuration is still applied. This makes the feature opt-out rather than opt-in, improving security by default. The default_csp_directives function provides a sensible starting point. Note: 'unsafe-inline' for script-src and style-src is a common necessity for many modern SPAs and hydration patterns but should be scrutinized and narrowed down (e.g., using nonces) in a production environment. For an SSG with partial hydration, it might be unavoidable without significant refactoring to use nonces or hashes.

Now, your config.toml can look like this:

# config.toml
base_url = "http://localhost:8080"
site_name = "My Secure SSG Site"
content_dir = "content"
output_dir = "public"
templates_dir = "templates"
static_dir = "static"
default_template = "base.html"

[csp]
enabled = true
# Example overrides or additions
[csp.directives]
script-src = "'self' 'unsafe-inline' https://cdn.example.com"
img-src = "'self' data: https://images.example.com"
report-uri = "/csp-report-endpoint" # Optional: where to send CSP violation reports

b) Core Implementation

We will implement two main security features: HTML sanitization and CSP generation.

Step 1: HTML Sanitization (XSS Prevention)

We’ll create a new src/security.rs module and add a function for sanitizing HTML. Then, we’ll integrate this function into our content processing pipeline, specifically after Markdown has been converted to HTML.

1. Create src/security.rs:

// src/security.rs
use ammonia::Ammonia;
use log::{debug, warn};
use std::collections::HashSet;

/// Sanitizes an HTML string to prevent XSS attacks.
///
/// This function uses the `ammonia` crate to clean HTML by removing
/// potentially dangerous tags and attributes.
///
/// # Arguments
/// * `html` - The HTML string to sanitize.
///
/// # Returns
/// A sanitized HTML string.
pub fn sanitize_html(html: &str) -> String {
    debug!("Sanitizing HTML content...");

    // Configure ammonia to allow common HTML tags and attributes
    // This is a default configuration. You might want to make this configurable
    // based on the specific needs of your site.
    let mut cleaner = Ammonia::new();

    // Default allowed tags (e.g., for rich text content)
    let mut tags = HashSet::new();
    tags.extend(vec![
        "a", "abbr", "b", "blockquote", "br", "code", "em", "h1", "h2", "h3", "h4", "h5", "h6",
        "hr", "i", "li", "ol", "p", "pre", "strong", "ul", "img", "span", "div", "table", "tbody",
        "td", "th", "thead", "tr", "s", "u", "del", "ins", "sup", "sub", "details", "summary",
        "figcaption", "figure", "cite", "dl", "dt", "dd", "mark", "q", "time", "video", "audio",
        "source", "track", "iframe" // iframe needs careful consideration with CSP
    ].into_iter().map(String::from));

    // Default allowed attributes
    let mut attrs = HashSet::new();
    attrs.extend(vec![
        "class", "id", "style", "title", "lang", "dir", "href", "src", "alt", "width", "height",
        "loading", "target", "rel", "aria-label", "aria-hidden", "controls", "autoplay", "loop",
        "muted", "poster", "preload", "data-src", "data-hydration-id", "data-component" // Custom attributes for hydration/components
    ].into_iter().map(String::from));

    // Allowed protocols
    let mut protocols = HashSet::new();
    protocols.extend(vec![
        "http", "https", "mailto", "tel", "#"
    ].into_iter().map(String::from));

    // Customize the cleaner
    cleaner.tags(tags);
    cleaner.attrs(attrs);
    cleaner.url_schemes(protocols);
    cleaner.link_rel(Some("noopener noreferrer".to_string())); // Good practice for external links

    let sanitized_html = cleaner.clean(html).to_string();
    if sanitized_html != html {
        debug!("HTML content was modified during sanitization.");
    } else {
        debug!("HTML content remained unchanged after sanitization.");
    }
    sanitized_html
}

/// Generates a Content Security Policy (CSP) meta tag string.
///
/// # Arguments
/// * `config_csp` - The CSP configuration from the SSG's main config.
///
/// # Returns
/// An `Option<String>` containing the CSP meta tag if CSP is enabled, otherwise `None`.
pub fn generate_csp_meta_tag(config_csp: &crate::config::CspConfig) -> Option<String> {
    if !config_csp.enabled {
        debug!("CSP is disabled in configuration.");
        return None;
    }

    let directives: Vec<String> = config_csp
        .directives
        .iter()
        .map(|(key, value)| format!("{} {}", key, value))
        .collect();

    let csp_value = directives.join("; ");
    if csp_value.is_empty() {
        warn!("CSP is enabled but no directives are defined. This will result in an empty policy.");
        None
    } else {
        debug!("Generated CSP: {}", csp_value);
        Some(format!("<meta http-equiv=\"Content-Security-Policy\" content=\"{}\">", csp_value))
    }
}

Explanation:

  • sanitize_html function: Takes an HTML string and uses ammonia to clean it. We define a set of allowed tags, attributes, and URL schemes. This is a crucial step to prevent XSS. For instance, ammonia will strip <script> tags, onclick attributes, and other dangerous constructs by default. We’ve added custom attributes like data-hydration-id and data-component which are essential for our component hydration strategy.
  • generate_csp_meta_tag function: Constructs the CSP meta tag string based on the CspConfig provided. It checks if CSP is enabled and then iterates through the directives to build the content attribute value.

2. Integrate Sanitization into Content Processing:

Now, we need to call sanitize_html where our Markdown is converted to HTML. This typically happens in your content_processor module or similar, after pulldown-cmark has done its job.

Let’s assume you have a function like process_markdown_to_html in src/content_processor.rs.

// src/content_processor.rs
use pulldown_cmark::{Parser, Options, html};
use log::{debug, error};
use crate::security; // Import our new security module

/// Processes Markdown content, converting it to HTML and sanitizing it.
///
/// # Arguments
/// * `markdown_input` - The Markdown string to process.
///
/// # Returns
/// A `Result<String, String>` containing the sanitized HTML or an error message.
pub fn process_markdown_to_html(markdown_input: &str) -> Result<String, String> {
    debug!("Starting Markdown to HTML conversion...");

    let mut options = Options::empty();
    options.insert(Options::ENABLE_TABLES);
    options.insert(Options::ENABLE_FOOTNOTES);
    options.insert(Options::ENABLE_STRIKETHROUGH);
    options.insert(Options::ENABLE_TASKLISTS);
    options.insert(Options::ENABLE_SMART_PUNCTUATION);

    let parser = Parser::new_ext(markdown_input, options);

    let mut html_output = String::new();
    html::push_html(&mut html_output, parser);

    debug!("Markdown converted to raw HTML. Now sanitizing...");
    let sanitized_html = security::sanitize_html(&html_output);
    debug!("HTML sanitization complete.");

    Ok(sanitized_html)
}

Explanation: The process_markdown_to_html function now performs the following sequence:

  1. Parses Markdown using pulldown-cmark.
  2. Generates raw HTML.
  3. Calls security::sanitize_html on the raw HTML.
  4. Returns the sanitized HTML.

Any component-specific Markdown processing or custom syntax parsing should also eventually feed into this sanitize_html function if it produces HTML that could contain user-controlled content.

Step 2: Content Security Policy (CSP) Integration

We need to add the generated CSP meta tag to the <head> section of our final HTML output. This is typically done in the templating layer, where the full page HTML is assembled.

Let’s assume your main rendering logic is in src/renderer.rs and uses Tera.

1. Modify src/renderer.rs (or your main template rendering logic):

// src/renderer.rs
use tera::{Tera, Context};
use std::path::{Path, PathBuf};
use std::fs;
use log::{debug, error};
use crate::config::Config;
use crate::security; // Import security module

pub struct PageContext {
    pub content: String,
    pub frontmatter: tera::Context,
    // Add other page-specific data
}

pub struct Renderer {
    tera: Tera,
    config: Config,
}

impl Renderer {
    pub fn new(config: Config) -> Result<Self, Box<dyn std::error::Error>> {
        let mut tera = Tera::new(&format!("{}/**/*.html", config.templates_dir.to_str().unwrap()))?;
        tera.autoescape_on(vec![".html"]); // Ensure autoescaping is on for default templates
        Ok(Renderer { tera, config })
    }

    /// Renders a single page with its content and context into a final HTML string.
    pub fn render_page(&self, page_context: PageContext, template_name: &str) -> Result<String, Box<dyn std::error::Error>> {
        let mut context = Context::new();
        context.insert("page", &page_context.frontmatter); // Page frontmatter
        context.insert("content", &page_context.content); // Page HTML content
        context.insert("config", &self.config); // Global config

        // New: Generate and insert CSP meta tag
        if let Some(csp_meta_tag) = security::generate_csp_meta_tag(&self.config.csp) {
            debug!("Inserting CSP meta tag into template context.");
            context.insert("csp_meta_tag", &csp_meta_tag);
        } else {
            debug!("CSP meta tag not generated (either disabled or empty directives).");
            context.insert("csp_meta_tag", ""); // Ensure variable exists even if empty
        }

        debug!("Rendering template: {}", template_name);
        let rendered_html = self.tera.render(template_name, &context)?;
        Ok(rendered_html)
    }

    // ... other rendering functions like render_index, render_taxonomy, etc.
}

Explanation: In the render_page method, we now call security::generate_csp_meta_tag using our application’s Config. If a CSP meta tag is generated, it’s inserted into the Tera Context under the key csp_meta_tag.

2. Update your Tera base template:

Finally, in your base HTML template (e.g., templates/base.html), you need to insert the csp_meta_tag variable within the <head> section.

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="{{ config.language | default(value='en') }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ page.title }} - {{ config.site_name }}</title>
    
    <!-- New: Insert CSP meta tag here -->
    {{ csp_meta_tag | safe }} 

    <link rel="stylesheet" href="/static/css/main.css">
    <!-- ... other head elements ... -->
</head>
<body>
    <header>
        <h1><a href="/">{{ config.site_name }}</a></h1>
        <nav>
            <!-- ... navigation ... -->
        </nav>
    </header>
    <main>
        {% block content %}
            {{ content | safe }}
        {% endblock content %}
    </main>
    <footer>
        <p>&copy; {{ "now()" | date(format="%Y") }} {{ config.site_name }}</p>
    </footer>
    <script src="/static/js/main.js"></script>
    {% block scripts %}
    {% endblock scripts %}
</body>
</html>

Why | safe filter? The | safe filter in Tera is crucial here. By default, Tera (and other templating engines) will HTML-escape all variables to prevent XSS. However, our csp_meta_tag is already a properly formed HTML string, and escaping it would turn < into &lt;, rendering it useless. We explicitly tell Tera that this content is “safe” and should not be escaped. This is generally safe because we are generating the CSP string in our Rust code, not taking it directly from untrusted user input.

c) Testing This Component

After implementing these changes, it’s vital to test them.

1. Testing HTML Sanitization:

  • Craft a malicious Markdown file: Create a new Markdown file (e.g., content/malicious.md) with the following content:

    +++
    title = "Malicious Content Test"
    +++
    
    # This is a test for XSS
    
    <script>alert('XSS Attack!');</script>
    
    <img src="x" onerror="alert('Image XSS!');">
    
    <a href="javascript:alert('Link XSS!')">Click me</a>
    
    <p style="color: red; background-image: url(javascript:alert('CSS XSS!'))">Dangerous Style</p>
    
    <div>Normal content here.</div>
    
  • Run your SSG build: Execute your SSG to generate the static files.

  • Inspect the output: Open public/malicious/index.html (or wherever it’s generated).

    • Expected Behavior: You should not see any alert pop-ups when opening the HTML file in a browser.
    • Verify Source Code: Open the generated index.html in a text editor.
      • The <script> tag should be entirely removed.
      • The onerror attribute on the <img> tag should be removed.
      • The javascript: protocol in the <a> tag’s href should be removed or converted to about:blank.
      • The background-image CSS property with javascript: should be removed.
      • The <div>Normal content here.</div> should remain.

2. Testing Content Security Policy (CSP):

  • Ensure CSP is enabled: Check your config.toml to confirm csp.enabled = true and that csp.directives has some values.
  • Run your SSG build: Generate the static files.
  • Inspect the output: Open any generated HTML file (e.g., public/index.html) in a browser.
    • Expected Behavior:
      • Open your browser’s developer tools (F12).
      • Go to the “Network” tab or “Security” tab.
      • You should see a Content-Security-Policy meta tag in the <head> of the rendered HTML.
      • If you deliberately try to violate the policy (e.g., by adding an inline script without 'unsafe-inline' in your script-src directive), you should see warnings or errors in the browser’s console indicating a CSP violation.
    • Verify Meta Tag: In the “Elements” tab of developer tools, expand the <head> section and confirm the presence and correctness of the <meta http-equiv="Content-Security-Policy" content="..."> tag.

Debugging Tips:

  • Sanitization: If malicious content isn’t removed, double-check your ammonia configuration in src/security.rs for allowed tags/attributes. Ensure the sanitize_html function is actually being called in your content processing pipeline.
  • CSP: If the meta tag isn’t appearing, check src/config.rs for csp.enabled and src/renderer.rs to ensure csp_meta_tag is being inserted into the Tera context and then rendered with | safe. If CSP is too strict and breaks functionality, examine the browser console for CSP violation reports to identify which directive needs adjustment. Use report-uri in your CSP to get real-time reports from users.

Production Considerations

Beyond the core implementations, several other aspects are critical for production-ready security.

Error Handling

  • Sanitization Errors: ammonia is robust and generally doesn’t “fail” in a way that needs explicit Result handling, but it’s good to log when sanitization occurs (as we did with debug! messages) to understand its impact.
  • CSP Configuration Errors: If the config.toml contains malformed CSP directives, generate_csp_meta_tag will still produce a string. The browser will then silently ignore invalid directives. Robust logging around CSP parsing and generation is useful to catch user-configuration mistakes.

Performance Optimization

  • Sanitization Overhead: For very large sites with many pages, HTML sanitization can add a measurable overhead to build times.
    • Caching: If you implement incremental builds (as discussed in a previous chapter), ensure that sanitized content is cached. Only re-sanitize pages that have changed.
    • Selective Sanitization: Consider if all content needs sanitization. For example, if you know certain content sources are absolutely trusted and only Markdown is used, you might opt out of sanitization for those specific content types, but this introduces risk. Generally, it’s safer to sanitize everything.

Security Best Practices

  • Secure Dependency Management:
    • Regularly run cargo audit in your CI/CD pipeline. This tool checks your Cargo.lock against the RustSec Advisory Database for known vulnerabilities in your dependencies.
    • Keep dependencies updated. Use cargo update frequently and review changelogs.
    • Consider using cargo deny for more granular control over allowed licenses, advisories, and publishing.
  • Build Environment Security:
    • Ensure your CI/CD environment (where the SSG builds the site) is secure. Use ephemeral build agents.
    • Do not store sensitive credentials (API keys, deployment tokens) directly in your repository. Use environment variables or a secret management system.
  • Deployment Security:
    • HTTPS/TLS: Always deploy your static site over HTTPS. This encrypts communication between the user and your server, protecting data integrity and user privacy. Most CDNs and hosting providers offer free TLS certificates (e.g., Let’s Encrypt).
    • HTTP Security Headers: In addition to CSP, configure other essential HTTP security headers via your CDN or web server (Nginx, Apache, Cloudflare, Netlify, Vercel, etc.):
      • Strict-Transport-Security (HSTS): Forces browsers to use HTTPS.
      • X-Content-Type-Options: nosniff: Prevents browsers from MIME-sniffing a response away from the declared content-type.
      • X-Frame-Options: DENY (or SAMEORIGIN): Prevents clickjacking by controlling if your site can be embedded in an <iframe>.
      • Referrer-Policy: no-referrer-when-downgrade (or stricter): Controls how much referrer information is sent with requests.
      • Permissions-Policy: (Formerly Feature-Policy) Allows or disallows the use of browser features.
    • File Permissions: Ensure generated static files on the server have appropriate read-only permissions.
    • CDN Security: Leverage CDN features for DDoS protection, WAF (Web Application Firewall), and edge caching security.
  • Client-Side Script Security:
    • If your SSG supports partial hydration with client-side JavaScript, ensure those scripts are also reviewed for vulnerabilities.
    • Minimize third-party scripts. If unavoidable, load them with async or defer and evaluate their security posture.
    • Avoid direct DOM manipulation with user-provided data without proper escaping.

Logging and Monitoring

  • CSP Violation Reports: Configure a report-uri directive in your CSP. This endpoint will receive JSON reports from browsers whenever a CSP violation occurs. Monitor these reports to identify potential attacks or misconfigurations.
  • Build Logs: Ensure your build process logs any warnings or errors, especially those related to security (e.g., cargo audit reports).

Code Review Checkpoint

At this point, you should have implemented:

  • Cargo.toml: Added ammonia dependency.
  • src/config.rs: Updated Config struct with CspConfig to define enabled and directives for CSP.
  • src/security.rs:
    • sanitize_html function using ammonia for XSS prevention.
    • generate_csp_meta_tag function to construct the CSP <meta> tag.
  • src/content_processor.rs (or similar): Integrated security::sanitize_html after Markdown-to-HTML conversion.
  • src/renderer.rs (or similar): Integrated security::generate_csp_meta_tag into the rendering context.
  • templates/base.html (or your main layout): Added {{ csp_meta_tag | safe }} within the <head> section.

These changes significantly improve the security posture of the generated static sites by addressing client-side vulnerabilities directly within the SSG’s build pipeline.

Common Issues & Solutions

  1. Issue: Over-sanitization of legitimate HTML/JS.

    • Problem: ammonia might remove tags or attributes that you intend to keep for specific functionality (e.g., certain data-* attributes for your hydration components, or iframes from trusted sources).
    • Solution: Carefully review the Ammonia configuration in src/security.rs. Use cleaner.tags(), cleaner.attrs(), and cleaner.url_schemes() to explicitly allow necessary elements. Test extensively with your actual content to ensure no critical functionality is broken. For iframe, consider if you can use a stricter sandbox attribute or only allow them from specific trusted domains via CSP.
  2. Issue: Content Security Policy (CSP) breaks site functionality.

    • Problem: A strict CSP can prevent legitimate scripts, styles, images, or fonts from loading if their source doesn’t match a directive. This is a very common initial problem when implementing CSP. For example, if you use Google Fonts, you need to add fonts.googleapis.com to font-src. If you have inline scripts for hydration, you might need 'unsafe-inline' or a more secure nonce/hash approach (which is more complex to implement in an SSG).
    • Solution:
      • Start with report-only: Initially, deploy CSP in report-only mode (Content-Security-Policy-Report-Only). This reports violations to your report-uri without blocking content, allowing you to identify all necessary sources.
      • Inspect browser console: The browser’s developer console will show CSP violation errors, indicating which directive is being violated and by which resource.
      • Iteratively refine: Add necessary domains/sources to your CSP directives one by one until all legitimate resources load.
      • Avoid 'unsafe-inline' where possible: While convenient for inline scripts/styles, it weakens CSP. For production, consider using nonce attributes or SHA hashes for inline scripts, which requires more sophisticated integration with your SSG’s rendering. For this tutorial, 'unsafe-inline' might be a necessary starting point for partial hydration scripts.
  3. Issue: Outdated or vulnerable dependencies detected by cargo audit.

    • Problem: cargo audit reports vulnerabilities in your project’s dependencies.
    • Solution:
      • Update dependencies: The first step is usually to run cargo update and then cargo audit again. Many vulnerabilities are fixed in newer patch versions.
      • Review advisories: If updating doesn’t fix it, read the RustSec advisory carefully. It might suggest a minimum version, a workaround, or indicate that the vulnerability doesn’t affect your specific usage.
      • Replace dependency: If a dependency is unmaintained or has persistent vulnerabilities, you may need to find an alternative crate.
      • Yank/patch: In rare cases, you might need to use cargo yank or [patch] directives in Cargo.toml if a fix isn’t available.

Testing & Verification

To fully verify the security enhancements:

  1. Build your SSG: Run your build command (cargo run -- build).
  2. Open the generated site: Navigate to public/index.html (or any other generated page) in a modern web browser.
  3. Content Sanitization Check:
    • Open the malicious.md page you created for testing.
    • Verify that no alert pop-ups appear.
    • Inspect the page source (right-click -> “View Page Source”) and confirm that the malicious HTML elements (e.g., <script>, onerror attributes, javascript: links) have been removed or neutralized.
  4. CSP Check:
    • Open your browser’s developer tools (F12).
    • Go to the “Elements” tab and confirm the presence of the <meta http-equiv="Content-Security-Policy" ...> tag in the <head>.
    • Go to the “Console” tab. If you have a correctly configured CSP, you should see no CSP violation errors for your legitimate site content. If you deliberately try to load an external script not in your script-src directive, you should see a violation.
    • Go to the “Network” tab. Observe that all resources (scripts, styles, images) are loaded from allowed sources as defined by your CSP.
  5. Dependency Security Check (Manual):
    • Run cargo audit in your project root.
    • Verify that there are no known vulnerabilities reported for your dependencies. If there are, address them.

By performing these checks, you can be confident that your SSG is generating more secure static sites.

Summary & Next Steps

In this chapter, we significantly bolstered the security posture of our Rust SSG. We integrated ammonia for robust HTML sanitization to prevent XSS attacks arising from user-provided content. We also added the capability to generate a Content Security Policy (CSP) meta tag, allowing site administrators to define strict rules for resource loading and execution, thereby mitigating various injection attacks. Finally, we discussed critical production-ready security considerations, including secure dependency management, build environment security, and deployment best practices like HTTPS, HSTS, and other HTTP security headers.

You now have a deeper understanding of how to proactively integrate security into your static site generation process, making your content platform more resilient against common web vulnerabilities.

In the next chapter, we will shift our focus to Chapter 18: Implementing Search Indexing with Pagefind. This will involve integrating a client-side search solution to make your generated content easily discoverable, providing a powerful feature for documentation sites and large content platforms.