Q-Putaway – Security
Controls implemented
| Threat | Control |
|---|---|
| SQL injection | PDO prepared statements with native prepares (ATTR_EMULATE_PREPARES=false). Identifiers come only from code. Grid sort/filter columns are whitelisted |
| XSS | Context-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 |
| CSRF | Per-session synchronizer token required on every POST (form field _csrf or X-CSRF-Token header). SameSite=Lax cookies |
| Broken access control / IDOR | Route-level permissions plus service-level checks. Every query on warehouse data is restricted to the user's warehouses (Auth::warehouseScope, requireWarehouse) |
| Privilege escalation | Users 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/hijacking | Session 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 force | Per-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 storage | password_hash() (bcrypt/argon2 per PHP default), rehash on login. Never logged, never in cookies or browser storage |
| Remember Me | Selector + 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 |
| 2FA | TOTP (RFC 6238); secrets encrypted with AES-256-GCM |
| Replay / double submit | Idempotency keys on stock-changing endpoints (stored response replayed, concurrent duplicates get 409) |
| Race conditions | InnoDB row locks (FOR UPDATE) on balances, reservations, documents and tasks. Verified with multi-process tests (tests/concurrency/race.php) |
| Mass assignment | Inputs are mapped through explicit field lists / master definitions |
| File uploads | Size 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 traversal | Stored names validated by strict regex. Docs viewer whitelists docs/*.md. Updates reject .., absolute paths and protected targets |
| CSV formula injection | Exported cells beginning with = + - @ TAB CR (non-numeric) are prefixed with ' |
| Information disclosure | display_errors off in production; errors logged with a reference code; no SQL or stack traces in responses; private folders blocked by .htaccess |
| Unauthorised backups/restore | Super Admin only, recent re-authentication, typed confirmation, HMAC-verified files only, automatic pre-restore backup. Never executes arbitrary uploaded SQL |
| Insecure updates | Ed25519-signed manifests, per-file SHA-256, version compatibility checks, staged apply, rollback archive, Super Admin + audit |
| Clickjacking / sniffing | X-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) inconfig/config.phpderives purpose-specific AES-256-GCM keys (settings secrets such as the SMTP password, TOTP secrets, HMAC signing).backup.keyis a separate key for backup encryption. Keep an offline copy separately from the backups; without it, encrypted backups cannot be restored.config/config.phpis 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.