Give feedback as an early user and get the docs plus a full website free. Code FRIEND: valid on one-time purchases only.

Security in Eleventy

Eleventy is a pure static site generator: every page is rendered to plain HTML at build time, so a finished site has no server runtime, no database, and almost nothing to attack.

Purely Static

Eleventy only ever outputs HTML, CSS, and JS — nothing running at request time. Just files on a CDN.

Escaped by Default

Output in .html templates is HTML-escaped automatically, blocking most XSS.

Secrets Stay at Build Time

process.env is read only during the Node build. Nothing reaches the browser unless you print it.

A Minimal Attack Surface

Because the output is just files, the whole class of server-side attacks simply does not apply.

A traditional CMS exposes a database, an admin login, and a stack of plugins that all run on every request — each one a potential entry point for SQL injection, remote code execution, or privilege escalation. An Eleventy site has none of that at runtime. Visitors receive pre-built files from a CDN, so there is no live application server to compromise.

Removed by static output

  • • SQL injection — no database queries
  • • Server RCE — no production runtime to execute on
  • • Vulnerable CMS plugins — none installed
  • • Admin-panel brute forcing — the Decap login is delegated off-site

Handled by Netlify

  • • Automatic HTTPS / TLS certificates
  • • DDoS protection at the edge
  • • Distributed, cached delivery (no single server to take down)
  • • Instant rollbacks to a previous deploy

Your build is the runtime. The one place code actually executes is the build — on your machine or in CI. Treat that environment as production: keep build secrets in CI, review every plugin, and never run a build from an untrusted source.

XSS Protection & the safe filter

Nunjucks escapes template output for you. The only common way to open an XSS hole is to opt out.

Any value you render with {{ }} in a Nunjucks template is HTML-escaped, so a string like <script> is shown as text instead of executed. The escape hatch is the safe filter — it outputs raw HTML, so only ever pass it content you trust or have sanitized.

{# userBio comes from a CMS or user-generated content, untrusted #}

{# ✅ Safe: Nunjucks HTML-escapes automatically #}
<p>{{ userBio }}</p>

{# ❌ Dangerous: `safe` outputs raw HTML, bypassing escaping #}
<p>{{ userBio | safe }}</p>

When you genuinely need to render user-supplied HTML (a rich-text bio, Markdown from an untrusted source), sanitize it first. Add a filter in your config backed by a library like sanitize-html, then pipe through it before safe.

// .eleventy.js
const sanitizeHtml = require("sanitize-html");

module.exports = function (eleventyConfig) {
  eleventyConfig.addFilter("sanitize", (html) => sanitizeHtml(html));
};

Now <p>{{ userBio | sanitize | safe }}</p> renders the HTML raw, but stripped of scripts and event handlers first.

Environment Variables & Secrets

Eleventy runs in Node at build time, so process.env is available where you build — never in the browser.

You can read process.env in your config, in global data files, and in JavaScript templates — all of which run only during the build. None of it ships to visitors unless you explicitly print it into a page. The starter already relies on this pattern: it reads ELEVENTY_ENV (set to PROD on Netlify builds) to toggle minification. Use dotenv for local values and keep your .env file out of git.

# .env  (add to .gitignore, never commit secrets)
API_TOKEN=secret_xxxxx          # used at build time to fetch data
ANALYTICS_ID=G-XXXXXXX          # safe to print into the page

A global data file in src/_data/ — alongside the kit's own client.js — can use a secret to fetch from an API while only the returned data — never the token — ends up in your pages:

// src/_data/products.js , runs at build time only
module.exports = async function () {
  const res = await fetch("https://api.example.com/products", {
    headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
  });
  // Only this returned data is rendered, the token never reaches the page
  return res.json();
};

Never print a secret into a template — even in a comment or a data- attribute. Anything you output ends up in the public HTML. Build-time secrets must stay in build-time code (config, _data, filters), never in the rendered page.

Forms & Dynamic Features Live Off-Site

With no server of your own, there is nothing to receive a form POST — so whole classes of attack are never yours to defend.

Because a finished Eleventy site is just files, dynamic work like contact forms, payments, or search is delegated to specialized providers — Netlify Forms, Formspree, Stripe, and the like. They handle spam filtering, validation, and CSRF on their own hardened infrastructure, keeping sensitive operations off your public site entirely.

<!-- A Netlify form, submissions are processed by Netlify, not by you -->
<form name="contact" method="POST" data-netlify="true">
  <input type="hidden" name="form-name" value="contact" />
  <label>Email <input type="email" name="email" required /></label>
  <label>Message <textarea name="message" required></textarea></label>
  <button type="submit">Send</button>
</form>

Pick a reputable provider and let it do the hard part. By keeping submissions, payment details, and authentication on a maintained third-party platform, your site keeps a strong security posture without ever exposing a server.

The Decap CMS Admin

The one login in this kit lives at /admin/ — but the authentication never runs on your site.

The starter bundles Decap CMS so a client can edit blog posts. It looks like an admin panel, but it is just static HTML and JavaScript: it has no server, no database, and no password store of its own. Logins are delegated to DecapBridge, and approved edits are committed straight to your Git repository — which then triggers a fresh Netlify build. There is nothing on your public site to brute-force.

Why it stays safe

  • • Auth handled by DecapBridge, not your site
  • • Edits land as Git commits, gated by repo access
  • • No CMS database or runtime to exploit
  • • Content is re-validated on the next build

Your responsibilities

  • • Limit who has write access to the repo
  • • Treat editor accounts like deploy keys
  • • Sanitize CMS-authored HTML before safe
  • • Run npm run remove-decap if unused

Because content authored in Decap is “trusted-ish” user input, the same escaping rules apply: render Markdown bodies normally, and if you ever pipe CMS HTML through safe, sanitize it first. Not building a blog? The kit's remove-decap script strips the admin entirely, shrinking the attack surface to nothing but static files.

Security Headers & CSP

A handful of HTTP headers hardens any static site — and you set them at your host.

Since Eleventy ships only files, response headers are configured where you deploy. This starter deploys to Netlify, which reads a _headers file from your output folder (drop it in src/ so it copies into public/, next to the kit's existing _redirects).

# src/_headers  (copied into public/, served by Netlify)
/*
  Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  X-Frame-Options: DENY
  Permissions-Policy: geolocation=(), microphone=(), camera=()

Prefer config over a copied file? The starter already ships a netlify.toml at the repo root — add a headers block to it:

# netlify.toml
[[headers]]
  for = "/*"
  [headers.values]
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"
    X-Frame-Options = "DENY"

Content Security Policy

Eleventy does not generate CSP hashes for you, so the cleanest path is to avoid inline scripts and styles — then you can ship a strict Content-Security-Policy header (alongside the others above) without unsafe-inline. If you must use inline code, generate a hash or nonce and add it to the policy. A CSP is your strongest defense-in-depth layer against XSS, blocking unauthorized scripts even if one slips through.

Validate Data at Build Time

Wherever you pull in outside data — APIs, src/_data files, front matter — validate it before you trust it.

Eleventy assembles data from many sources at build time. Validating anything from an external source means a bad upstream response fails the build instead of shipping broken or unsafe markup. A schema library like zod works well inside a data file:

// src/_data/posts.js , validate external data before it's rendered
const { z } = require("zod");

const PostSchema = z.array(
  z.object({
    title: z.string().min(1),
    slug: z.string().regex(/^[a-z0-9-]+$/),
    body: z.string(),
  })
);

module.exports = async function () {
  const res = await fetch("https://cms.example.com/posts");
  // Throws and fails the build if the shape is wrong
  return PostSchema.parse(await res.json());
};

Keep Dependencies Current

The biggest remaining risk for a static site is the npm packages it is built from.

Eleventy and its plugins are your supply chain — each one runs with full access during the build. Audit them regularly, keep a committed lockfile so builds are reproducible, and be deliberate about which plugins you add.

npm audit                          # report known vulnerabilities
npm audit fix                      # apply safe, compatible fixes
npm outdated                       # see which packages have newer versions
npm install @11ty/eleventy@latest  # update Eleventy itself