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:
Key Security Steps:
- 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.
- 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. - Dependency Scanning: While not directly part of the SSG’s runtime, integrating tools like
cargo auditinto our CI/CD pipeline is critical for ensuring that our SSG itself (and its dependencies) are free from known vulnerabilities. - 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_htmlfunction: Takes an HTML string and usesammoniato clean it. We define a set of allowed tags, attributes, and URL schemes. This is a crucial step to prevent XSS. For instance,ammoniawill strip<script>tags,onclickattributes, and other dangerous constructs by default. We’ve added custom attributes likedata-hydration-idanddata-componentwhich are essential for our component hydration strategy.generate_csp_meta_tagfunction: Constructs the CSP meta tag string based on theCspConfigprovided. It checks if CSP is enabled and then iterates through the directives to build thecontentattribute 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:
- Parses Markdown using
pulldown-cmark. - Generates raw HTML.
- Calls
security::sanitize_htmlon the raw HTML. - 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>© {{ "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 <, 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
alertpop-ups when opening the HTML file in a browser. - Verify Source Code: Open the generated
index.htmlin a text editor.- The
<script>tag should be entirely removed. - The
onerrorattribute on the<img>tag should be removed. - The
javascript:protocol in the<a>tag’shrefshould be removed or converted toabout:blank. - The
background-imageCSS property withjavascript:should be removed. - The
<div>Normal content here.</div>should remain.
- The
- Expected Behavior: You should not see any
2. Testing Content Security Policy (CSP):
- Ensure CSP is enabled: Check your
config.tomlto confirmcsp.enabled = trueand thatcsp.directiveshas 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-Policymeta 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 yourscript-srcdirective), 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.
- Expected Behavior:
Debugging Tips:
- Sanitization: If malicious content isn’t removed, double-check your
ammoniaconfiguration insrc/security.rsfor allowed tags/attributes. Ensure thesanitize_htmlfunction is actually being called in your content processing pipeline. - CSP: If the meta tag isn’t appearing, check
src/config.rsforcsp.enabledandsrc/renderer.rsto ensurecsp_meta_tagis 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. Usereport-uriin 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:
ammoniais robust and generally doesn’t “fail” in a way that needs explicitResulthandling, but it’s good to log when sanitization occurs (as we did withdebug!messages) to understand its impact. - CSP Configuration Errors: If the
config.tomlcontains malformed CSP directives,generate_csp_meta_tagwill 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 auditin your CI/CD pipeline. This tool checks yourCargo.lockagainst the RustSec Advisory Database for known vulnerabilities in your dependencies. - Keep dependencies updated. Use
cargo updatefrequently and review changelogs. - Consider using
cargo denyfor more granular control over allowed licenses, advisories, and publishing.
- Regularly run
- 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(orSAMEORIGIN): 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
asyncordeferand evaluate their security posture. - Avoid direct DOM manipulation with user-provided data without proper escaping.
Logging and Monitoring
- CSP Violation Reports: Configure a
report-uridirective 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 auditreports).
Code Review Checkpoint
At this point, you should have implemented:
Cargo.toml: Addedammoniadependency.src/config.rs: UpdatedConfigstruct withCspConfigto defineenabledanddirectivesfor CSP.src/security.rs:sanitize_htmlfunction usingammoniafor XSS prevention.generate_csp_meta_tagfunction to construct the CSP<meta>tag.
src/content_processor.rs(or similar): Integratedsecurity::sanitize_htmlafter Markdown-to-HTML conversion.src/renderer.rs(or similar): Integratedsecurity::generate_csp_meta_taginto 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
Issue: Over-sanitization of legitimate HTML/JS.
- Problem:
ammoniamight remove tags or attributes that you intend to keep for specific functionality (e.g., certaindata-*attributes for your hydration components, or iframes from trusted sources). - Solution: Carefully review the
Ammoniaconfiguration insrc/security.rs. Usecleaner.tags(),cleaner.attrs(), andcleaner.url_schemes()to explicitly allow necessary elements. Test extensively with your actual content to ensure no critical functionality is broken. Foriframe, consider if you can use a strictersandboxattribute or only allow them from specific trusted domains via CSP.
- Problem:
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.comtofont-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 inreport-onlymode (Content-Security-Policy-Report-Only). This reports violations to yourreport-uriwithout 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 usingnonceattributes 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.
- Start with
- 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
Issue: Outdated or vulnerable dependencies detected by
cargo audit.- Problem:
cargo auditreports vulnerabilities in your project’s dependencies. - Solution:
- Update dependencies: The first step is usually to run
cargo updateand thencargo auditagain. 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 yankor[patch]directives inCargo.tomlif a fix isn’t available.
- Update dependencies: The first step is usually to run
- Problem:
Testing & Verification
To fully verify the security enhancements:
- Build your SSG: Run your build command (
cargo run -- build). - Open the generated site: Navigate to
public/index.html(or any other generated page) in a modern web browser. - Content Sanitization Check:
- Open the
malicious.mdpage you created for testing. - Verify that no
alertpop-ups appear. - Inspect the page source (right-click -> “View Page Source”) and confirm that the malicious HTML elements (e.g.,
<script>,onerrorattributes,javascript:links) have been removed or neutralized.
- Open the
- 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-srcdirective, 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.
- Dependency Security Check (Manual):
- Run
cargo auditin your project root. - Verify that there are no known vulnerabilities reported for your dependencies. If there are, address them.
- Run
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.