Security and privacy
MarkStack generates a static reader and optionally runs a separate authenticated writing server. Those are different security boundaries. Editor accounts control source-writing actions. They do not restrict who reads generated output. An internal handbook needs access control on its reader host/network and an actual unauthorized-reader denial test.
This is an implementation guide, not a security certification. Use the pinned engine, reviewed source, least necessary filesystem permissions, supported Node, private runtime storage and a correctly configured HTTPS proxy when writers connect remotely. A familiar product name, successful build or hidden sidebar section does not establish a protected audience.
Protect each layer
| Layer | Protection you must supply or verify |
|---|---|
| Markdown/source | Intended writers, review, source history and backups |
| Public assets | Every copied byte is suitable for the reader audience |
| Private runtime | Account hashes/state/history outside public roots, restricted ownership/modes and private backups |
| Editor listener | Deliberate reachability, accepted host names, HTTPS/proxy configuration and tested roles |
| Generated reader | Host/network authentication when needed; no stock reader login |
| Browser profile | Private device/profile, local draft cleanup and trusted extensions |
Upload only reviewed generated output, not the whole checkout. static/ is a public input directory: files there are copied regardless of whether their names look private. .gitignore is not a publication filter. A hidden section still builds and can appear in search. Search index text itself can disclose generated content.
Private settings/link values can intentionally appear in output; account hashes and operational history must not. Review downloads, image metadata, page examples, diagnostics and fictional screenshot data as carefully as ordinary prose. A source repository being private does not protect files already deployed publicly.
Roles and content authorization
| Action | Admin | Editor |
|---|---|---|
| Root pages | Yes | No |
| Section descendants | Any section | Assigned only |
| Rename or delete section root | Yes | No |
| Publish | Yes | No |
| Manage users, settings and links | Yes | No |
| Own password change | Yes | Yes |
An assignable section is a real top-level content folder with a valid lowercase name. Editors may work inside their assigned sections, including descendants, but cannot rename/delete/write the section folder itself or rename into another section. Root pages are outside assignments. Unauthorized section paths generally return 404 so another section’s file names are not exposed; invalid/outside-root/forbidden operations can return 400 or 403 according to their actual checks.
Path checks resolve the deepest existing ancestor and symbolic links to enforce containment. Catalog scans skip symlinks. These checks do not turn arbitrary source imports into a trusted build input: an operator who writes the source tree still controls what the builder reads. Keep source ownership narrow and inspect imported trees/media before building.
The tree and creation workflow favor Markdown. However, read/save APIs do not universally enforce a .md suffix. Do not describe the server as a Markdown-extension-only security barrier. The actual boundaries are authorized user/role/path and resolved filesystem containment. An authenticated administrator has broad source access within that boundary.
Test an editor from a separate browser session: save one allowed page, deny another section/root page, deny Admin/publication, then recheck after changing assignments. Administrator visibility is not an editor permission test. See Accounts and access for every account action and session effect.
Password policy and stored hashes
| Property | Rule |
|---|---|
| Accepted password | Text, length 12-256, not in the fixed common-password list |
| Length | JavaScript string length before hash normalization |
| Mix | No required mix of symbols, digits or cases |
| NFKC | NFKC before scrypt derivation |
| Default scrypt | N=65536, r=8, p=1, key 32 bytes, random salt 16 bytes |
| Stored form | Algorithm, cost, salt and derived key; no plaintext |
Unknown accounts perform a dummy password hash rather than a simple immediate success/failure distinction. Verification bounds stored scrypt parameters to resist absurd memory costs from a tampered account file. That does not protect against an operator who can replace account state, read private files or change running code. OS/service access is its own authority boundary.
Create/recover passwords through the hidden CLI prompt or an appropriately protected automation stdin path. Never put them into command arguments, URLs, screenshots, source or logs. A fixed sample password in public docs invites reuse; examples should name fields/commands, not provide a credential everyone shares.
Admin shell recovery resets an existing administrator, enables it, clears forced change and raises its access version. Browser editor reset instead issues a temporary password for an editor and requires change at sign-in. Role/name changes and account deletion are not general browser features. Keep a recoverable administrator and private account backup rather than relying on an undocumented repair.
Sessions and cookies
| Property | Default/behavior |
|---|---|
| Idle expiry | 480 minutes since last successful session read |
| Absolute expiry | 10080 minutes from creation |
| Session identifier | Random 32-byte token in cookie; SHA-256 key stored in session map |
| CSRF value | Random 24-byte value per session |
| Cookie flags | HttpOnly, SameSite=Strict, Path=/, absolute-lifetime Max-Age |
| Secure mode | Enabled only when secure environment value is exactly 1 |
| Session persistence | Memory only; restart ends every session |
Plain local HTTP uses ms_session. Secure mode uses __Host-ms_session, adds Secure, has no Domain attribute and expires the old unprefixed cookie. The browser’s __Host- rules restrict sibling-domain cookie planting, but require a suitable secure connection. Enabling Secure while browsing ordinary HTTP can prevent successful sign-in. Do not disable production protection to diagnose a wrongly configured URL/proxy.
Server idle/absolute validation still matters even while a cookie exists. Normal session reads refresh idle time; viewing a session summary does not. Summary exposes creation/last-active timestamps, not cookie hashes/CSRF values. Account disable/password/assignment changes raise access version and invalidate old sessions. Own password change replaces this browser’s session/CSRF while ending older sessions. Sign out destroys the selected session; Sign out everywhere raises account version and ends that user’s sessions, including your own if you target yourself.
In-memory storage is not a guarantee that the raw token never appears anywhere in process memory: request processing necessarily handles it. The in-process session map stores its hash, not a persistent session file. Do not infer immunity to process compromise or logging mistakes from hashed map keys. Browser Markdown drafts can remain after sign-out and are separate from server session lifetime.
Request gates
| Gate | Actual behavior |
|---|---|
| Host | Exact lowercased allowlist; local host:port defaults plus configured extra values |
| Origin | When present, parsed origin host must match request Host |
| POST body type | application/json, optional media-type parameters accepted |
| Body size | Default 2 MiB; rejected oversized request returns 413 |
| JSON parse | Invalid JSON returns 400 |
| Session | All non-login APIs require usable signed-in session |
| CSRF | Every non-GET authenticated API request must provide its per-session CSRF header |
| Role/path | Each API applies its required administrator or content authorization |
Host values include the intended port when it is not implicit. A wildcard domain is not implemented. The Origin comparison is host/port-based, not a full scheme-equality claim; absent Origin is not automatically refused. The CSRF session header is a separate gate. Login is public but still receives Host/Origin/POST-JSON checks.
A forced password change allows only account identity, logout and password-change APIs until completed. A writer’s page opening may redirect to login while an API returns 401; an administrator-only page/API can return 403. These are not interchangeable symptoms.
The editor serves its CSS/JS/SVG/vendor/static routes without a session, after global request gates. Do not place private files under static assets expecting account protection. The reader has its own hosting headers; editor CSP/security headers do not automatically attach to deployed static pages.
Failed-login limits
| Counter | Threshold and block |
|---|---|
| Account plus address | 5 failed attempts blocks that pair |
| One address | 30 failures across accounts blocks that address |
| One account across addresses | 20 failures pauses unfamiliar addresses for that account |
| Window/block | 15 minutes by default |
The limiter tracks a window from its first failure, expiring it when the window elapses; do not promise a continuously sliding per-attempt window just because a source comment uses that phrase. Once a threshold is met, a block runs from that failure. Successful sign-in clears the account/address counter, not every global/address counter.
For the account-wide pause, up to five addresses previously successful for that account remain familiar. This exception does not bypass an address or pair block. Familiar addresses, counters and sessions are memory-only and reset at restart. Restarting to clear a warning also ends every session and does not repair a weak exposure/credential/proxy configuration.
429 reports a blocked attempt;401 reports failed credentials. Avoid repetitive retries. Check actual account/name/store, secure-cookie URL and expiry before trying again. No user-name/client-address counter keys are exposed in the Security summary. Audit events still need private handling.
Trusted forwarding proxies
By default, the socket peer address is the client address; forwarded headers are ignored. Configured trusted proxies are comma-separated exact addresses, with the special loopback value covering IPv4/IPv6 loopback. This is not arbitrary CIDR/wildcard trust.
Only when the connection peer is trusted does the server walk X-Forwarded-For right-to-left, skipping trusted hops and selecting the first other address. An empty/all-trusted forwarded chain falls back to the socket peer. Trust only proxies actually under your control and configure them to replace/append forwarding data correctly. Otherwise attackers may influence rate-limit identity or every writer may appear to share one address.
Proxy trust affects client-address accounting; it does not terminate TLS or configure the allowed host list. Configure Host/HTTPS/Secure cookie/proxy addresses together and test positive and denied cases. See HTTPS proxy.
HTML, frontmatter and preview
Raw authored HTML is disabled unless file policy is actual boolean true. Runtime Admin branding cannot enable it. If enabled, finished pages can contain active HTML/resources, so trust every writer/import and review the host CSP. A sandboxed preview does not guarantee the finished public reader is safe.
Frontmatter permits YAML only, refusing executable/alternate languages such as JavaScript/JSON markers. This blocks the old gray-matter executable-language route; it does not mean malformed YAML or hostile/invalid content cannot fail a build. Unknown YAML fields do not create permissions, draft states or external workflows.
Editor preview runs inside an iframe with allow-same-origin but without allow-scripts/popups. Scripts/event handlers in preview content cannot simply execute there. Mermaid is rendered by the parent pipeline in strict mode. Preview and built-reader plugins differ; validate final output rather than interpreting a harmless-looking preview as a security review.
Editor/login/Admin responses include intended CSP and security headers such as nosniff, frame denial and no-referrer. Stock static output needs the host’s own policy. External media and user-supplied raw HTML can change network/security behavior beyond local font/library bundles.
Network and audit boundaries
| Operation | Possible external activity |
|---|---|
| Fresh install | Repository/dependency downloads |
| Bundled reader libraries/fonts | Served locally from generated assets |
| Authored remote media | Reader contacts that resource host |
| New/refresh header icon | Editor server fetches destination under URL/DNS/size/timeout constraints |
| Cached shown icons | Served as local selected output files |
Favicon fetch validates HTTP/HTTPS, standard ports, no credentials, public resolved addresses and redirects, and pins the chosen connection address. It has finite size/time/candidate budgets and rejects SVG icon content. A failed icon lookup need not fail link saving. That controlled exception is not permission to use the editor as an arbitrary private-network fetch service.
The audit is bounded local operational history, not a tamper-proof/compliance ledger. Not every page save/file CRUD event is recorded. Files can be changed by their operator, old archives rotate, and failed audit writes do not automatically reverse successful account/settings/publication actions. Keep errors and actual state together. Diagnostics may contain usernames/paths even after recognized secret patterns are redacted.
Not built in
HTTPS termination, reader authentication, SSO, MFA, central directory synchronization, distributed publisher locking, HA/clustering, real-time document collaboration, page version/undo and a compliance-grade audit service are not provided by this engine. External controls may cover your requirements, but their installation and tests are not MarkStack feature claims.
For organizational deployment, define who writes, reviews, publishes, operates the host and restores backups. Verify external access controls from a genuinely unauthorized session, rehearse restore in staging and measure load. Do not infer an enterprise availability/security guarantee from the documentation’s depth.
Security acceptance checks
- Review source and every copied public asset for its intended audience.
- Keep private runtime and backups outside public roots with correct ownership/ACLs.
- Verify supported Node, loopback/private binding, exact hosts and actual HTTPS cookie behavior.
- Test administrator/editor allowed and denied actions on synthetic pages.
- Recheck session invalidation after password/access/disable/reset actions.
- Test independent reader-host denial if the reader is internal.
- Inspect remote media/icon behavior and scrub diagnostics before external sharing.
- Rehearse source/private-runtime restore and check actual service state, not only HTML availability.
The account/request/path/limiter/session behaviors are source-backed and regression-tested. Actual production TLS/proxy/reader access control, operating-system ACLs and every organization-specific threat model require checks in your environment. See Private team editor, Backup and restore and Editor and account issues.