Security Notes
Updated: 2026-09-04
The September 2026 security/UI hardening is deployed and established. The older notes that called the audit/revision migrations pending are obsolete. PR #196 contained the main hardening implementation; PR #199 corrected new FK columns to match the existing signed INT recipe/user IDs.
This document records security behavior that must be preserved during v1.0 productization and future upgrades.
Authentication and passwords
- Use PHP
password_hash()/password_verify(). - Never store, log, or email plain-text passwords.
- Normal user-selected passwords require at least 12 characters.
- Password changes/resets revoke persistent remembered-login tokens.
- Long-lived login cookies use random selector/validator credentials, one-way validator hashes server-side, appropriate HttpOnly/SameSite/Secure attributes, expiry, and rotation.
- Login attempts must remain rate-limited.
Account-request email verification
Optional email verification uses a cryptographically random, single-use token. Only a SHA-256 token hash is stored; verification links expire after 24 hours and the stored token hash is cleared after successful use. If verification is required, an unverified request cannot be approved either through the UI or by the server-side approval action.
SMTP passwords and other mail credentials remain in .env, never in site_settings, audit logs, diagnostics, or repository history. A failed verification-email send must not leave an unusable new pending request.
Authorization must fail closed
Database-backed roles/permissions are authoritative on migrated installations.
- Permission lookup errors deny rather than broaden access.
- Owner protection remains explicit.
- Legacy fallback is only for genuine bootstrap/schema-absence compatibility, not ordinary failures.
- UI hiding is not authorization; sensitive routes enforce permission server-side.
- The last Owner must not be deletable, disabled, demoted, or stripped of critical permissions.
CSRF and HTTP methods
All state-changing operations use a non-GET method plus CSRF protection, including save/overwrite/import, account/admin changes, restore, logout, and other mutations. GET remains observational/navigation-only.
HTTPS, CSP, and headers
Real user access should use HTTPS. Proxy/tunnel HTTPS detection must match deployment so secure cookies are correct.
Established browser hardening includes:
- nonce-based CSP for maintained inline script/style blocks;
- no
unsafe-inlinein script/style policy; style-src-attr 'none'expectation;- one-year HSTS on HTTPS;
- production Apache signature/token reduction.
Do not weaken CSP to accommodate avoidable inline event handlers/style mutations.
Docker exposure
Expected safe bindings remain:
recipe_web 127.0.0.1:8080->80
recipe_phpmyadmin 127.0.0.1:8081->80
recipe_db internal only
Do not publish MariaDB or phpMyAdmin to the internet.
Public web tree
Do not leave helper scripts, backups, SQL dumps, patch/debug files, secrets, generated archives, or temporary tooling in www. Apache blocks sensitive internal paths/extensions, but unnecessary files should still be removed rather than relying on blocking rules.
The shared header/footer now provide a main-content landmark and keyboard skip link; preserve accessible focus/navigation behavior during UI changes.
Uploads and source imports
Treat all uploaded/imported material as untrusted.
Recipe images remain limited to validated raster formats such as JPEG/PNG/WebP/GIF where a specific path supports GIF. Do not allow PHP, shell, arbitrary HTML/SVG/executables in image/runtime paths. Use finfo, signature/dimension checks, generated filenames, safe confinement, and bounded sizes.
General source import currently accepts expected text/document/image formats including:
- TXT/HTML/HTM/RTF/DOCX;
- legacy Word
.docvia localantiword; - PDF via Poppler, with scanned pages routed through local Tesseract;
- JPEG/PNG/WebP OCR.
The production image deliberately uses small local extraction tools (antiword, Poppler, Tesseract) rather than a full office suite. Extraction does not imply trust: extension/MIME/size checks, time/resource bounds, temporary-file cleanup, and review-before-save remain mandatory.
OCR/PDF safety
PDF/scanned-image acquisition is bounded. Scanned PDFs are rasterized page-at-a-time rather than creating an unbounded multi-page image burst. OCR text is shown to the user for correction before parsing.
Column-layout choices affect reading order only; they do not bypass normal parser/review/save protections.
URL importer / SSRF protection
Required protections:
- HTTP/HTTPS only;
- no embedded credentials;
- reject localhost/private/reserved/internal targets;
- resolve DNS and validate public addresses;
- pin validated destination IP for the actual request;
- revalidate redirects;
- restrict curl protocols;
- cap redirects, connect/total time, and response bytes;
- accept only expected HTML/text types;
- never support
file://or arbitrary local filesystem fetches.
Do not weaken these controls because a public site returns 403.
Read-only parser/tool access
Where the web container needs parser/tool source, keep it read-only. Web requests must not be able to modify parser source code or deployment tooling.
Temporary source-import files
Use server-generated temporary names/paths. Clean temporary source archives, extracted documents, rasterized PDF pages, OCR/parser outputs, response bodies, and headers on success/failure. Do not retain arbitrary fetched/uploaded source material as repository artifacts.
Import preview rule
Parsing/acquisition never means approval to save. Preserve:
- no automatic publish/save;
- no silent overwrite;
- no silent local-category creation from foreign taxonomies;
- explicit duplicate handling;
- editable imported fields before explicit Save/Overwrite.
Audit logging
The audit_log foundation is live/established. Important authentication, account/admin, role/permission, site-setting, recipe/content, restore, and media operations may be audited.
Audit records must not contain passwords, password hashes, CSRF tokens, remember-token secrets, session IDs, credentials, or raw arbitrary uploads.
Site Health / diagnostics
Owner/admin-view Site Health diagnostics are established. Diagnostics should be observational by default and must not expose environment secrets, DB passwords, session material, raw credential files, or arbitrary file contents.
Revision history
Bounded recipe/content revision history and explicit restore are established. Restore must preserve/create history so it remains undoable. Revision access follows edit authorization.
Database migration compatibility
Foreign-key column type/signedness must exactly match referenced columns. The September hardening migration correctly stopped when INT UNSIGNED did not match existing signed INT; the correct fix was to correct the new migration, not casually alter established IDs.
Before meaningful DB writes:
- verify protected checkout;
- create and verify a fresh DB backup;
- inspect migration assumptions against live schema read-only where needed;
- apply required migrations in controlled order;
- deploy application code in the schema/code order required by the release;
- verify health/regressions afterward.
For v1.0+, this manual discipline must be encoded into the supported upgrader with applied-migration tracking.
Informational-section integrity
The old editor had a real integrity risk for unusual recipe_sections.section_type values. It is now fixed by structured versioned JSON transport rather than a whitelist mini-language. Current live rows pass exact type/title/body/order round-trip verification. Retained-backup comparison through 2026-08-26 found no evidence of missing unusual-section keys.
Server cleanup / operational hygiene
The September cleanup/retention pass is complete. Preserve the active runner installation/credentials, real backups, runtime media, protected live checkout/config, and unique historical material. Remove only proven stale/generated/redundant artifacts.
Runner retention is narrowly scoped to disposable areas:
_diag: 14 days;- inactive top-level
_work/_temp: 7 days.
Do not use runner cleanup to prune real site/database/media backups.
Final v1.0 security audit
Before release, re-audit:
- authentication/session/cookie behavior;
- authorization/Owner invariants;
- CSRF/non-GET mutation rules;
- rate limiting;
- upload/archive/document/PDF/OCR paths;
- URL SSRF controls;
- path traversal/confinement;
- SQL parameterization;
- XSS/output escaping;
- CSP/HSTS/error leakage;
- Docker/phpMyAdmin/DB exposure;
- runtime permissions;
- audit-log redaction;
- backup contents/protection;
- dependency/container versions;
- installer/upgrader security and migration ordering.
