EmDash CMS Security: How Your Data Is Protected

EmDash CMS Security: How Your Data Is Protected

Security claims are easy to make and hard to verify from marketing copy. This is a mechanism-by-mechanism breakdown of what EmDash actually does, sourced from its documented architecture rather than a vendor's summary paragraph.

Table of Contents
  1. Authentication: No Passwords to Leak
  2. Role-Based Access Control
  3. Plugin Security: Capability Declarations, Not Implicit Trust
  4. Secrets Encryption
  5. CSRF Protection on State-Changing Requests
  6. Rate Limiting
  7. Data Ownership: Self-Hosted Means You Control Location
  8. What EmDash Doesn't Provide Out of the Box
  9. Frequently Asked Questions
  10. Is passkey-only login a problem if a user loses their device?
  11. Can a malicious sandboxed plugin still do damage within its declared capabilities?
  12. Does self-hosting mean EmDash is inherently more secure than a SaaS CMS?
  13. Where can I report a security vulnerability in EmDash itself?
  14. The Bottom Line

Authentication: No Passwords to Leak

EmDash uses passkey authentication as its primary login method. Passkeys use WebAuthn, a web standard that creates public-key credentials stored on your device or synced through your password manager. When you log in, your device proves possession of the credential without ever sending a password over the network.

That last point is the real security property: there's no password transmitted, stored as a hash, or reused across sites for an attacker to steal via phishing or a database breach. Credentials are bound to your site's specific domain, which is what makes them phishing-resistant — a fake login page on a different domain simply can't use a passkey issued for your real one. Magic-link email login exists as a fallback for lost-device scenarios, and GitHub, Google, and Atmosphere (AT Protocol) are available as additional pluggable providers.

Role-Based Access Control

Five roles, from least to most access: Subscriber, Contributor, Author, Editor, Admin. Schema changes (creating or deleting collections and fields) and site settings updates require Admin specifically — a Contributor or Author literally cannot perform those operations, not just "shouldn't" by convention. Every API token and MCP-connected AI assistant operates under the same role-based permission the underlying user account has, so connecting an AI tool doesn't create a permission bypass.

Read also:

Plugin Security: Capability Declarations, Not Implicit Trust

Sandboxed plugins are isolated by default. To do anything beyond reading and writing their own KV and storage, a plugin has to declare a capability in its manifest. A plugin that didn't declare content:read doesn't get a ctx.content, and one that didn't declare network:request doesn't get ctx.http.

This is the specific architectural answer to the plugin-security problem that drives the majority of WordPress's documented vulnerability disclosures. A sandboxed EmDash plugin physically cannot reach the network, read arbitrary content, or access another plugin's data unless it explicitly declared that capability in its manifest — and that declaration is exactly what the admin's install consent screen shows a site operator before they approve it. Network access is further scoped to an explicit host allowlist by default; a plugin declaring `network:request` with `allowedHosts: ["api.example.com"]` cannot reach any other host, full stop.

Secrets Encryption

Plugin secrets (webhook tokens, third-party API keys, and similar) are encrypted at rest using a key you generate and control:

npx emdash secrets generate --write .env
emdashkits.com
The key is provided by you and never stored in the database; only encrypted ciphertext is. Losing it means losing every secret encrypted with it.

That last line is worth taking seriously operationally — back this key up somewhere durable (a password manager, a KMS, your team's secret store), since there's deliberately no recovery path if it's lost. That's a security trade-off made on purpose: a recoverable key would be a weaker one.

CSRF Protection on State-Changing Requests

Write operations (POST, PUT, PATCH, DELETE) on private plugin routes require both authentication and an `X-EmDash-Request` CSRF header, which the admin UI's own API client sends automatically. Token-authenticated requests (API tokens rather than browser sessions) are exempt from the CSRF check specifically because tokens aren't vulnerable to the cross-site request forgery attack pattern the header defends against — a deliberate, correct distinction rather than an oversight.

Rate Limiting

REST API endpoints support rate limiting based on deployment configuration, returning a 429 status with a `Retry-After` header when triggered — standard, real protection against abusive or runaway API usage rather than an unlimited attack surface.

Data Ownership: Self-Hosted Means You Control Location

Because EmDash is self-hosted, your content database and media storage live wherever you deploy them — a specific data-residency requirement (a particular country or region for compliance reasons) is something you control directly through your hosting and database choice, not something you're negotiating with a SaaS vendor's infrastructure decisions.

What EmDash Doesn't Provide Out of the Box

Being direct about real limits: EmDash doesn't currently hold named third-party compliance certifications (SOC 2, ISO 27001, HIPAA) the way some enterprise SaaS CMS platforms do — see our comparison to Kontent.ai, which specifically holds that certification portfolio, for organizations where that's a hard procurement requirement. EmDash's security model is architectural (sandboxing, capability scoping, passkey auth) rather than audited-and-certified, which is a genuinely different kind of assurance — strong technically, but not a substitute for a compliance certificate if your organization specifically requires one.

Frequently Asked Questions

Is passkey-only login a problem if a user loses their device?

No — magic link email authentication is a documented fallback specifically for this scenario, and additional passkeys can be registered per user across multiple devices, so losing one device doesn't lock someone out entirely.

Can a malicious sandboxed plugin still do damage within its declared capabilities?

Within what it declared and was approved for, yes in principle — a plugin with `content:write` could misuse that access. The sandbox's real value is preventing undeclared, silent overreach (a plugin claiming to be a simple widget secretly exfiltrating data over the network it never declared needing), not eliminating all possible misuse of legitimately granted permissions.

Does self-hosting mean EmDash is inherently more secure than a SaaS CMS?

Not automatically — self-hosting shifts security responsibility (patching, infrastructure hardening, monitoring) onto you, in exchange for direct control. A well-run SaaS platform with dedicated security staff can be more secure in practice than a self-hosted deployment nobody is actively maintaining.

Where can I report a security vulnerability in EmDash itself?

Plugin manifests require a security contact field for the plugin author; for the EmDash core project itself, check the project's GitHub repository for its current disclosure process, since that's the authoritative, up-to-date channel rather than anything static documentation might list.

The Bottom Line

EmDash's security model is genuinely architectural, not just a marketing claim — passkey authentication removes password-based attack vectors, sandboxed plugins with declared capabilities prevent undeclared overreach, and secrets are encrypted at rest with a key you own. What it doesn't offer yet is named third-party compliance certification, which matters specifically for regulated-industry procurement requirements. See our full EmDash CMS review for how this weighs against the platform's other trade-offs.

Share

Comments

Write a comment

Related Articles

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

July 16, 2026

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

July 16, 2026

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

TL;DR — A custom session-cookie login flow appeared to succeed on localhost (the OTP verified, the response looked fine) but every subsequent request to a login-gated page treated the visitor as logged out. Identical code worked fine on the live HTTPS site. The cookie's Secure attribute was hardcoded to true — and per the cookie spec, browsers never store or send a Secure cookie over a plain, non-HTTPS connection.

Table of Contents
  1. Diagnostic
  2. Root cause
  3. Fix
  4. Lessons learned

Diagnostic

Check the actual Set-Cookie response header and the browser's own cookie storage panel — on localhost over http://, the cookie is sent by the server but never actually stored by the browser.

Root cause

// before -- assumes the app is always served over HTTPS
setCookie("session", token, { secure: true, httpOnly: true });
emdashkits.com

A cookie config that quietly assumes "we're always on HTTPS" breaks the instant you test over plain HTTP, which local dev servers commonly are.

Read also:

Fix

// after -- derive secure from the actual request protocol
const isHttps = request.url.startsWith("https://");
setCookie("session", token, { secure: isHttps, httpOnly: true });
emdashkits.com

Lessons learned

  • Any Secure-flagged cookie needs to key off the real request scheme, not an assumption baked in once at cookie-creation time.
  • "Works in production, silently fails in local dev" is a strong signal to check cookie flags before anything else in an auth flow.
  • Check other cookies in the same codebase for the same hardcoded assumption — if one cookie has this bug, sibling cookies set the same way are worth auditing too.
Share
Previous Article

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Comments

Write a comment

Related Articles

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

July 16, 2026

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

July 16, 2026

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

TL;DR — GA4's gtag.js snippet was installed correctly, the page loaded with no visible JavaScript error, and GA4's real-time report still showed zero activity. The CMS's default Content-Security-Policy had no allowance for analytics domains and no config option to add one — so every request to Google's tracking endpoints was blocked at the browser level before it could fail loudly.

Table of Contents
  1. Diagnostic
  2. Root cause
  3. Lessons learned

Diagnostic

Check the browser's dedicated CSP violation reporting, not the regular console error list — CSP blocks are reported through their own channel, not thrown as normal script errors, so "no console errors" doesn't mean nothing was blocked.

Root cause

The CSP's script-src and connect-src directives had no entry for googletagmanager.com or google-analytics.com, and the CMS exposed no configuration surface to add one — the only way in was patching the CSP directives directly.

// patch-package: add analytics domains to the existing CSP directives
scriptSrc.push("https://www.googletagmanager.com");
connectSrc.push("https://www.google-analytics.com", "https://www.googletagmanager.com");
emdashkits.com
Read also:

Lessons learned

  • "No console errors" is not proof nothing was blocked — CSP violations live in their own reporting surface and are easy to miss if you're only scanning for red error text.
  • Before adding any third-party script tag to a site with a CSP already in place, check the CSP's directives first rather than assuming a silently-empty analytics dashboard means a snippet-installation mistake.
Share
Previous Article

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Next Article

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Comments

Write a comment

Related Articles

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

July 16, 2026

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

July 16, 2026

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)