Q-Putaway – Security

Controls implemented

ThreatControl
SQL injectionPDO prepared statements with native prepares (ATTR_EMULATE_PREPARES=false). Identifiers come only from code. Grid sort/filter columns are whitelisted
XSSContext-aware escaping (e() in PHP, QP.esc() in JS). Help articles sanitised with an allow-list DOM sanitizer on save and on render. Strict CSP without inline scripts
CSRFPer-session synchronizer token required on every POST (form field _csrf or X-CSRF-Token header). SameSite=Lax cookies
Broken access control / IDORRoute-level permissions plus service-level checks. Every query on warehouse data is restricted to the user's warehouses (Auth::warehouseScope, requireWarehouse)
Privilege escalationUsers cannot change their own role/status or grant permissions they lack. Only a Super Admin manages Super Admins. The last active Super Admin is protected
Session fixation/hijackingSession ID regenerated on login and privilege changes. HttpOnly + Secure (on HTTPS) + SameSite cookies. Strict session mode. session_version invalidates other sessions after a password change
Brute forcePer-account lockout (default 5 failures / 15 minutes) and a per-IP limit (30 failures / 15 minutes). Generic error messages (no user enumeration). Throttled password-reset requests
Password storagepassword_hash() (bcrypt/argon2 per PHP default), rehash on login. Never logged, never in cookies or browser storage
Remember MeSelector + validator cookie. Only the SHA-256 of the validator is stored. The validator rotates on every use. A mismatch (theft) revokes all of the user's devices. Revoked on logout, reset and password change
2FATOTP (RFC 6238); secrets encrypted with AES-256-GCM
Replay / double submitIdempotency keys on stock-changing endpoints (stored response replayed, concurrent duplicates get 409)
Race conditionsInnoDB row locks (FOR UPDATE) on balances, reservations, documents and tasks. Verified with multi-process tests (tests/concurrency/race.php)
Mass assignmentInputs are mapped through explicit field lists / master definitions
File uploadsSize limits; MIME sniffed with finfo (extension ignored); images verified; random names; stored in storage/uploads (not web-accessible) and served through an authenticated controller with nosniff
Path traversalStored names validated by strict regex. Docs viewer whitelists docs/*.md. Updates reject .., absolute paths and protected targets
CSV formula injectionExported cells beginning with = + - @ TAB CR (non-numeric) are prefixed with '
Information disclosuredisplay_errors off in production; errors logged with a reference code; no SQL or stack traces in responses; private folders blocked by .htaccess
Unauthorised backups/restoreSuper Admin only, recent re-authentication, typed confirmation, HMAC-verified files only, automatic pre-restore backup. Never executes arbitrary uploaded SQL
Insecure updatesEd25519-signed manifests, per-file SHA-256, version compatibility checks, staged apply, rollback archive, Super Admin + audit
Clickjacking / sniffingX-Frame-Options: DENY, frame-ancestors 'none', X-Content-Type-Options: nosniff, Referrer-Policy: same-origin, HSTS on HTTPS

Content-Security-Policy

default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:;
media-src 'self' blob:; font-src 'self' data:; connect-src 'self'; worker-src 'self'; manifest-src 'self';
frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'self'

style-src 'unsafe-inline' is needed for Bootstrap components and label sizing. Scripts never run inline.

Encryption and keys

  • app.key (32 random bytes, base64) in config/config.php derives purpose-specific AES-256-GCM keys (settings secrets such as the SMTP password, TOTP secrets, HMAC signing).
  • backup.key is a separate key for backup encryption. Keep an offline copy separately from the backups; without it, encrypted backups cannot be restored.
  • config/config.php is outside web access (.htaccess + a PHP guard). Never commit it.
  • Base64 is never used as encryption. Passwords are hashed, not encrypted.

Audit log

Recorded: logins/lockouts/2FA and password events, permission and user changes, master data create/update (field diffs), every stock document action, imports and exports, backups/restores, settings and email changes, and updates. Secrets (passwords, tokens, keys) are scrubbed before logging. There is no UI to edit or delete audit rows.

Verified by tests

tests/security/SecurityTest.php (lockout, enumeration, remember-me theft detection and rotation, session invalidation, CSRF, permissions, default role grants, SQL injection in grid parameters, mass assignment, escaping, CSV injection, path traversal, AES-GCM tamper detection, encrypted secrets, TOTP), tests/http/smoke.php (headers, CSRF, 401/419, private files, installer lock, open redirect) and tests/concurrency/race.php. See DEVELOPMENT_STATUS.md for the latest results.

Operational recommendations

  • Enable HTTPS and Force HTTPS; enable 2FA for Super Admins.
  • Keep PHP updated (hPanel → PHP Configuration).
  • Download backups regularly and keep the backup key offline.
  • Review the audit log and failed-login counts on the dashboard.
  • Remove departed users promptly (deactivate; their sessions end on the next request).

Reporting a vulnerability

Contact the system owner/administrator of your installation. Do not test against production without permission.