# Security review and test status

**Reviewed:** 7 October 2026  
**Scope:** focused source review of the PHP/MySQL starter, its public verification, authentication/RBAC, audit handling, template uploads, and production deployment defaults. This is an internal engineering review—not a penetration test, independent audit, legal/privacy opinion, or Ministry security approval.

## Issues found and changes made

1. **Database exceptions could be displayed to staff.** `PDOException` extends `RuntimeException` in PHP, so the previous action handler could classify SQL/connection failures as user-safe validation messages. The handler now exposes only messages from the exact base `RuntimeException` class and returns a generic message for PDO and other internal errors. Detailed errors remain in protected server logs.
2. **Public certificate-number lookup could reveal an unissued business record.** The public query now requires a matching certificate row, and lookup inputs are limited to certificate-number or 64-character hexadecimal-code formats. Unissued businesses are no longer returned by the public verifier.
3. **Last-active-Super-Admin protection was vulnerable to concurrent requests.** User role/deactivation changes now run transactionally, lock active Super Admin rows in a stable order, re-check the actor's current role inside the transaction, and commit the audit entry with the change.
4. **Audit rows duplicated sensitive business values.** Changed owner name, phone, email, street address, CAC number and TIN are now recorded as changed/redacted rather than old/new values. Return-for-correction text is kept on the business record rather than copied into the audit summary/JSON.
5. **Template settings were trusted when rendered.** Uploaded raster images are checked for supported MIME/dimensions and bounded pixel count; stored data URIs are validated again before rendering. Template preview colors/fonts are allow-listed at output. SVG uploads remain rejected.
6. **Misconfigured verification links could be issued over HTTP or to an invalid host.** Production certificate URLs now require a valid canonical HTTPS `APP_BASE_URL`. Optional MySQL CA/client TLS settings are supported; the preflight requires a readable CA for non-local DB hosts.
7. **Operational DB grants were broader than necessary.** The deployment runbook now specifies table-level grants. The runtime DB account can insert but not update/delete audit rows; its only `DELETE` grant is on `login_attempts`.
8. Added a CLI-only maintenance command to purge login-attempt records older than 24 hours and documented scheduling it under the service account.
9. Sign-out now clears the session even if the audit insert fails, and the Nginx sample redirects to the configured canonical host rather than reflecting an arbitrary request Host header.

## Automated checks

Run the dependency-free suite from the project root:

```sh
php tests/run.php
```

The suite covers RBAC expectations, HTML escaping, template color/position/image validation, audit redaction, public lookup formats and non-disclosure contract, form validation and date edges, CSRF token shape, generic internal-error messages, canonical HTTPS verification URLs, selected security-header/private-file contracts, password-strength policy, and forced-first-sign-in contracts. It performs no database writes and needs no test framework package.

In addition, a read-only smoke test against the sandbox MariaDB verified that an issued certificate resolves while an unissued business is hidden, and that the active-admin locking query executes successfully inside a rolled-back transaction. A disposable synthetic account was also used for an HTTP/MySQL password-flow smoke: first-sign-in access was gated, the CSRF-protected change persisted the new hash and cleared the flag, and dashboard access resumed; the temporary account and its audit/login-attempt rows were removed afterward. These checks are not a substitute for full concurrent workflow testing on the deployed database engine, browser-based upload testing, or network/host penetration testing. Those remain required before real records are loaded.

## Important residual risks / go-live gates

- Self-service password change and forced first-sign-in change for staff accounts created in the UI are now implemented. The application still has no Admin password-reset/recovery flow or MFA. The initial Super Admin created via CLI chooses a final password; apply the migration and set `must_change_password=1` manually if that account must rotate on first sign-in.
- Public verification by sequential certificate number is part of the requested workflow. Only issued-certificate fields are returned, but enumeration of issued registrations remains possible. Confirm the disclosure policy and consider request throttling if required.
- The Director role can register/edit as well as verify records. If Ministry policy requires strict maker-checker separation, add a record-level rule preventing the creator/last editor from approving their own record and test it with the actual staff workflow.
- Supporting documents are stored privately and streamed as downloads, but there is no malware scanning service. Add an approved scanning/quarantine process before accepting real documents.
- TLS redirects/HSTS, firewall/network isolation, PHP-FPM permissions, secret storage, log retention, backup encryption/restores and operational monitoring must be validated on the actual host. The CLI preflight cannot verify those controls.
- New audit entries redact selected fields, but prior audit rows in an existing database are not sanitized automatically. Review and apply the Ministry's retention policy before migrating an older database. Audit logs are append-only to the application account only when the documented DB grants are used; a privileged DBA or host administrator can still alter them. Back them up and monitor privileged access.
- The public page intentionally reveals business name, registration number, business type/category, LGA, issue/expiry dates and status for issued certificates. Confirm that exact disclosure with the Ministry before deployment.
- The Composer advisory check is point-in-time and must be repeated as part of release maintenance.

**Result:** focused code-level issues above were fixed and a 23-test unit/security-contract suite was added. Production use remains gated on independent review, Ministry privacy/policy approval, and host-level controls.
