Generate Report →

Zero-Database Security: Killing CMS Vulnerabilities With Static Sites

A static site has no database, no PHP runtime and no admin login — which deletes SQL injection, plugin RCE and credential stuffing outright. Here is the full threat model.

Every WordPress security guide is a list of things you must keep doing forever: patch core weekly, audit plugins, rotate salts, install a WAF, monitor for injected admin users. The advice is sound, and it exists because the architecture leaves a database, a PHP interpreter, and a public login form exposed to the internet at every moment.

Static architecture takes a different approach to the same problem. Instead of defending the runtime, it deletes the runtime. There is no database to inject into, no interpreter to execute an uploaded payload, and no /wp-admin for a botnet to brute-force. What ships to the edge is a directory of files.

This piece walks the actual threat model — what disappears, what genuinely remains, and how to configure the headers and pipeline that protect what is left.

What actually disappears when the database goes

The clearest way to see the difference is to map the request path. A dynamic CMS resolves a page by executing code against a database on every single request:

DYNAMIC CMS REQUEST PATH
Browser ─► CDN ─► Origin server ─► PHP-FPM ─► Plugin stack ─► MySQL
                     │                │            │            │
                  [SSH keys]     [RCE, LFI]  [plugin CVEs] [SQLi, dump]
                  /wp-admin  ──► [brute force, credential stuffing]

STATIC SITE REQUEST PATH
Browser ─► Cloudflare edge ─► static file
                             (no execution)

Build happens elsewhere, on a schedule you control:
Git push ─► CI runner ─► hugo --minify ─► upload artifact
        [supply chain: this is the real remaining surface]

Whole vulnerability classes vanish because their preconditions vanish:

  • SQL injection requires a query built at request time. No query, no injection.
  • Remote code execution via plugin requires an interpreter running third-party code. There is no interpreter.
  • Arbitrary file upload to a web-executable directory requires that directory to execute. Static hosts serve .php as a download or a 404.
  • Credential stuffing against the CMS login requires a public login. There isn’t one — publishing happens through Git.
  • Privilege escalation via user roles requires a user table. There is no user table.

That is not a hardening exercise. Those are structural impossibilities, and they hold without anyone remembering to patch anything.

The three risks that genuinely remain

Honest security writing names what is left. On a static stack, three things can still hurt you.

Supply chain compromise in the build. Your CI runner executes npm ci, pulls Hugo modules, and runs whatever those packages contain. A malicious transitive dependency can inject a crypto-miner or a credential exfiltrator into your generated HTML, and it will deploy to the edge with a valid signature and a green checkmark. Mitigations: commit lockfiles, pin GitHub Actions to full commit SHAs rather than mutable tags, and enable Dependabot on the build manifest.

Hosting and DNS account takeover. With no origin server to break into, the attacker’s cheapest path is your Cloudflare or GitHub account. Hardware security keys on both, plus scoped deploy tokens instead of personal access tokens, closes most of it. This is the risk that is now materially larger in relative terms — not because it grew, but because everything around it shrank.

Third-party client-side JavaScript. Analytics, chat widgets, and embedded forms run in your users’ browsers under your origin. A compromised widget host is a compromised site. This is precisely what a strict Content-Security-Policy exists to contain, and it is the one place where the static advantage requires active configuration rather than passive architecture.

Why a real CSP is only practical on a static site

Content-Security-Policy is widely recommended and rarely implemented well, because on a plugin-driven CMS you cannot enumerate what your own pages load. Some plugin somewhere emits an inline <script>, so you add unsafe-inline, and the policy stops preventing the attack it was written for.

On a static site you generated every byte, so you can be strict. A Cloudflare Pages _headers file:

/*
  Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self'; img-src 'self' data: https://www.google-analytics.com; connect-src 'self' https://www.google-analytics.com; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
  Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: geolocation=(), microphone=(), camera=(), interest-cohort=()

Three details are doing most of the work. object-src 'none' kills legacy plugin-embed vectors. base-uri 'self' blocks base-tag injection, which otherwise lets an attacker who achieves any HTML injection redirect every relative URL on the page. frame-ancestors 'none' ends clickjacking without needing the older X-Frame-Options.

Deploy in report-only mode first. Add Content-Security-Policy-Report-Only with the same policy, watch the violation reports for a week, then promote it. Skipping that step is how teams ship a policy that silently breaks their own analytics.

Comparing the two postures honestly

DimensionDynamic CMS (WordPress + plugins)Static (Hugo + Cloudflare Pages)
Public attack surfaceOrigin server, PHP, DB, admin loginCDN edge serving immutable files
Patch cadence requiredWeekly, indefinitelyOnly at build time, non-urgent
Typical CVE exposureCore plus every installed pluginEffectively zero in the serve path
Compromise blast radiusFull DB read/write, persistent backdoorRequires account or CI takeover
Recovery from compromiseRestore DB + files, rotate all secretsgit revert and redeploy
Realistic CSP strictnessunsafe-inline usually mandatoryNo unsafe-inline achievable
Cost of a WAF to compensateA recurring subscriptionNot needed for injection classes

The recovery row is the one operators underrate. When a static deploy is bad — compromised dependency, defaced content, anything — the fix is reverting a commit and rebuilding. Your entire site’s history is in Git, byte-for-byte reproducible. There is no “is the backdoor still in the database?” phase, because there is no database.

Keeping the features you thought needed a database

Migration usually stalls on three functions. Each has a mature static-compatible answer:

Contact forms. Post to a Cloudflare Worker, Formspree, Netlify Forms, or a Google Form. The submission endpoint holds the data; your origin stays read-only. Validate and rate-limit inside the Worker if you own it.

Site search. Generate a JSON index at build time and filter it client-side. For a few hundred pages this is instant and costs nothing. Past a few thousand, move to a hosted index such as Pagefind or Algolia — still no database on your side.

Comments. Use a hosted service, or accept that most business sites get more spam than discussion and drop them. This is often a feature removal rather than a loss.

Personalization. Handle at the edge with a Worker reading a cookie, not by rendering the page dynamically. The specifics of edge logic without giving up cacheability are covered in edge caching and Cloudflare WAF rules for static sites.

If you are coming from a heavy install, the content and URL-preservation side of the move is the harder half — see migrating heavy WordPress sites to Hugo for the redirect and taxonomy mapping work that has to happen alongside the security gain.

The search consequence nobody plans for

Security is not usually filed under SEO, right up until a compromise. Injected spam pages trigger a manual action; Safe Browsing flags produce an interstitial that destroys click-through; a hacked-content penalty takes weeks to lift after cleanup. The recovery window is the expensive part — removing the malware is the fast half, and re-earning Google’s trust is the slow one.

There is a quieter benefit too. AI crawlers and RAG pipelines have short patience — slow or intermittently erroring origins get partially indexed and then deprioritized. A static origin serving from the edge responds fast and effectively never 5xx’s, which means crawlers complete their traversal. Which of those crawlers you admit, and how you tell them apart, is set in robots.txt — see robots.txt directives for AI crawlers.

Next step

Enumerate your request path today. List every process that runs between the CDN and the byte your user receives: interpreter, plugin, database, cache layer. Anything on that list is something you are committing to patch forever. Then decide, for each item, whether it earns its place or whether a build-time equivalent exists.

For most content and local-business sites, the honest answer is that nothing on the list earns it. A MarketLens Standard audit will print the exact headers, exposed endpoints, and third-party script origins found on your live site so you can see the surface before you decide.

Run this article on your site

Review my website's security posture with the goal of moving to a zero-database static architecture. Enumerate every server-side runtime, plugin, and database dependency currently in the request path, classify each as removable or requiring an external service, then produce a Cloudflare Pages _headers file with a strict Content-Security-Policy (no unsafe-inline), HSTS with preload, X-Content-Type-Options, Referrer-Policy and a locked-down Permissions-Policy tailored to the scripts my site actually loads.

Paste into Claude Code, ChatGPT, Cursor or Gemini. It executes the steps above against your own site.

Frequently Asked Questions

Is a static site really immune to SQL injection?

Immune is the correct word for the classic case: there is no SQL database and no query being constructed at request time, so there is nothing to inject into. The caveat is that if you add a third-party form handler, comment service, or search API, that service has its own database and its own injection surface — the immunity applies to your origin, not to everything embedded in your pages.

What attack surface actually remains on a static site?

Three things: your build pipeline (a compromised npm or Hugo module dependency ships malicious code straight to production), your DNS and hosting account credentials, and any client-side JavaScript that calls third-party APIs. Roughly speaking you trade dozens of runtime CVEs for a small number of supply-chain and account-security risks.

Do I lose contact forms and search by dropping the database?

No, but you relocate them. Forms go to a hosted endpoint such as a Cloudflare Worker, Formspree, or a Google Form; search runs client-side over a JSON index generated at build time. Both patterns keep the origin read-only while preserving the functionality users expect.

How do security headers differ on a static host?

They get much stricter, because you know exactly what your pages load. With no plugin injecting inline scripts you can ship a Content-Security-Policy without unsafe-inline, set a long HSTS max-age with preload, and lock frame-ancestors to none — configurations that routinely break a live WordPress install.

Does better security help SEO or AI search visibility at all?

Indirectly but measurably. Compromised sites get flagged by Safe Browsing and deindexed, and injected spam pages trigger manual actions that take weeks to reverse. A site that has never been hacked accumulates uninterrupted crawl history, and AI crawlers that hit a fast, clean, always-200 origin index it more completely.

Continue the track — Static Architecture & Performance