# Security Documentation — Fon2Fix POS

Current security posture, patch inventory, and threat model. Companion docs: [SYSTEM.md](SYSTEM.md), [CHANGELOG.md](CHANGELOG.md), [OPERATIONS.md](OPERATIONS.md).

**Last hardening pass:** 2026-05-25 (v17.0)
**Next review due:** 2026-08-25 (quarterly)

---

## 1. Patch inventory — what was applied in v17.0

Each entry: *what changed, why, where, mitigations against regression.*

### CRITICAL severity

#### P-001 — Removed hardcoded DB password fallback
- **Where:** [db.php](db.php) (was lines 51-56), [sync_to_backup.php](sync_to_backup.php) (was lines 100-105)
- **Risk:** Plaintext DB password committed to source. Any leak (git history, backup, screen share) compromised the DB.
- **Patch:** Read precedence is now `env > config.php > fail-closed`. No hardcoded value remains. A missing/unreadable `config.php` aborts with a sanitized HTTP 500.
- **Verification:** `grep -R "lengkf880225" .` returns matches only inside `config.php` and `backups/pre_patch/*` (snapshots). No occurrences in any active PHP file.

#### P-002 — Removed hardcoded backup secret_key
- **Where:** [sync_to_backup.php](sync_to_backup.php) was hardcoding `secret_key = 'KDYOnu...'` at line 86 as a fallback.
- **Risk:** Anyone with read access to the source could forge backup uploads.
- **Patch:** Same env > config > fail-closed pattern. Script aborts when secret missing.

#### P-003 — Removed display_errors leak in backup.php
- **Where:** [backup.php](backup.php) had `ini_set('display_errors', 1)` at line 4.
- **Risk:** Backup failures leaked file paths, SQL, stack traces to the user.
- **Patch:** `display_errors = 0`; whole script wrapped in `try { ... } catch (\Throwable $e)`. User sees a friendly "Backup Failed" page; full trace goes to `error_log` only.

#### P-004 — Re-enabled SSL verification in uptime_monitor.php
- **Where:** [uptime_monitor.php](uptime_monitor.php) had `CURLOPT_SSL_VERIFYPEER = false` at line 33.
- **Risk:** MITM on the Telegram API call could intercept bot token + chat traffic.
- **Patch:** `VERIFYPEER = true`, `VERIFYHOST = 2`. System CA bundle (provided by cPanel) is used.

#### P-005 — Authenticated uptime_monitor.php
- **Where:** [uptime_monitor.php](uptime_monitor.php) had no access control; anyone could hit it.
- **Risk:** Trivial DoS vector and information disclosure.
- **Patch:** Requires `?access_token=` matching `config.backup_sync.monitor_token` (or env `FONFIX_MONITOR_TOKEN`). Compared via `hash_equals` (constant-time).

### HIGH severity

#### P-006 — `exec()` added to `disable_functions`
- **Where:** [php.ini](php.ini) line 21.
- **Risk:** Even though no current code calls native `exec()`, leaving it enabled meant any future RCE primitive (e.g. via a vulnerable dependency) could shell out.
- **Patch:** `disable_functions = passthru,shell_exec,system,proc_open,popen,exec`.
- **Compatibility note:** All `exec(` references in the codebase are `PDO::exec()` (method call, not function). Grep-verified across `api.php`, `dashboard.php`, `optimize_database.php`, `index.php`, `repairtotext.php`, `renumber_iv_orders.php`, `Archives-Executed/*`.

#### P-007 — `expose_php = Off`
- **Where:** [php.ini](php.ini).
- **Risk:** `X-Powered-By: PHP/8.x.y` header fingerprints the runtime, helping targeted exploits.
- **Patch:** PHP no longer announces itself. `.htaccess` additionally `unset`s the header at the Apache layer.

#### P-008 — Session hardening
- **Where:** [php.ini](php.ini), [db.php](db.php), [index.php](index.php).
- **Risk:** Long-lived sessions + lax cookie attributes + uninitialised SID = session-fixation surface.
- **Patches:**
  - `session.use_strict_mode = 1` rejects unknown session IDs.
  - `session.sid_length = 48` / `sid_bits_per_character = 6` → high-entropy IDs.
  - `session.cookie_secure = 1` (in php.ini) + secure flag computed at runtime when `HTTPS=on`.
  - `SameSite=Lax` standardised across `db.php` and `index.php` (previously `index.php` used `Strict`, which broke deep-links from WhatsApp).
  - `security.session_lifetime` in `config.php` makes the 100-year UX **opt-in** via config — default behaviour unchanged so staff aren't logged out.

#### P-009 — File-based IP rate limit
- **Where:** [api.php](api.php) lines 752-790 (was session-keyed).
- **Risk:** Old per-session counter could be bypassed by clearing cookies.
- **Patch:** Persisted per-IP counter under `backups/ratelimit/{sha1(ip)}.json`. 100 req/min cap. Atomic write with fallback (failure to persist does NOT break the API call).

#### P-010 — CSRF on optimize_database.php
- **Where:** [optimize_database.php](optimize_database.php) lines 33-46.
- **Risk:** A bookmarked `?confirm=yes` URL could be triggered cross-site (CSRF) by a victim admin.
- **Patch:** Index creation requires POST with valid `csrf_token`. GET fallback accepted only if `&csrf=` is also supplied (preserves bookmarks within the same browser session).

### MEDIUM severity

#### P-011 — Security headers expanded
- **Where:** [db.php](db.php), [index.php](index.php), [api.php](api.php), [.htaccess](.htaccess).
- **Patches:**
  - `Strict-Transport-Security: max-age=300` on HTTPS responses (short max-age to allow rollback; bump to 31536000 after a week of stable HTTPS per OPERATIONS.md).
  - `Permissions-Policy: geolocation=(), microphone=(), camera=(self)`.
  - `Content-Security-Policy-Report-Only` with explicit allow-list for the 5 active CDN origins (cdnjs, jsdelivr, unpkg) and `'unsafe-inline'` to coexist with 585 inline `onclick=`/`style=` attributes in `dashboard.php`.
  - `Referrer-Policy: strict-origin-when-cross-origin`.

#### P-012 — `.htaccess` defence-in-depth
- **Where:** new file at root.
- **Patches:**
  - Deny direct web access to `config.php`, `db.php`, `helpers.php`, `php.ini`, `.htaccess`, `database_dump.sql`, `debug_log.txt`, `monitor_backup_status.txt`.
  - Deny entire `backups/` and `Archives-Executed/` directories.
  - Deny all `.sql`, `.zip`, `.bak`, `.log`, `.env` files.
  - `Options -Indexes` (no directory listings).
  - Mirror of PHP-level bot UA block at the rewrite layer.
  - Gzip + browser cache for static assets.
  - Force-HTTPS rule present but **commented out**; enable after the cert/cron audit in OPERATIONS.md.

#### P-013 — CDN supply-chain hardening (SRI)
- **Where:** [dashboard.php](dashboard.php) lines 100-115.
- **Risk:** Two scripts were loading from floating CDN URLs (`https://cdn.jsdelivr.net/npm/chart.js` and `https://unpkg.com/html5-qrcode`) with no integrity check. Compromised CDN or version bump could silently replace the bundle.
- **Patches:**
  - Pinned to specific versions: Chart.js 4.5.0, html5-qrcode 2.3.8 (and the others kept at their existing pinned versions).
  - Added `integrity="sha512-..." crossorigin="anonymous"` to all 5 `<script>` tags. Hashes fetched from the cdnjs SRI API.

#### P-014 — Env-var precedence in helpers.getConfig
- **Where:** [helpers.php](helpers.php).
- **Patch:** `getConfig()` now reads `FONFIX_*` env vars before `config.php`. Allows operators to keep secrets out of source entirely on environments that support environment variables in cPanel.

### Informational / non-vulnerability changes

#### P-015 — Documentation refresh
- [README.md](README.md) PHP min bumped 7.4 → 8.1 (matches actual cPanel runtime).
- New docs: [SYSTEM.md](SYSTEM.md), [SECURITY.md](SECURITY.md), [CHANGELOG.md](CHANGELOG.md), [OPERATIONS.md](OPERATIONS.md).
- Existing [security_improvements.md](security_improvements.md) retained for history.

#### P-016 — Recurring checkup tooling
- New: `health_check.php`, `security_audit.php`, `log_rotate.php`, `cron_runner.php`.
- All gated by Master/Endie session OR a cron token. Read-only against business data (only writes to `backups/*` and rotates logs).

---

## 2. Current threat model

| Asset | Threat | Control |
|---|---|---|
| `users.password` | offline crack after DB dump | `password_hash()` (bcrypt) |
| Session cookie | XSS theft, MITM | HttpOnly, Secure on HTTPS, SameSite=Lax, `use_strict_mode`, regen on login |
| DB | SQL injection | PDO prepared statements throughout; auto-migrations are admin-only |
| DB | unauthorised read | network-private (localhost), `.htaccess` + perms 600 on config |
| API | abuse / scraping | file-based IP rate limit (100/min), bot UA block (Apache + PHP) |
| Login | brute force | per-IP+username table `login_attempts`, 5/15 min lockout |
| State-changing API | CSRF | session-bound token, validated via `hash_equals` |
| Static files | listing / direct fetch | `.htaccess` deny rules + `Options -Indexes` |
| CDN dependencies | supply-chain swap | pinned versions + SRI |
| Backup server | unauthorised restore | `secret_key` over HTTPS w/ verified peer |
| Backup zip | exposure during transit | HTTPS to off-site server; SSL verify on |
| Admin scripts | drive-by trigger | POST + CSRF for `optimize_database.php`; cron token for `health_check.php`, `log_rotate.php`, `security_audit.php`, `cron_runner.php` |
| Telegram bot | token disclosure | rotated post-deploy; stored in `config.php` (perms 600) |

---

## 3. Known residual risks

- **Inline JS/CSS in `dashboard.php`** prevents enforcing CSP. Currently in `report-only` mode. Mitigation plan: incremental migration of `onclick=`/`style=` to event listeners and stylesheets, then flip to enforced CSP.
- **`addslashes()` in `backup.php` SQL dump** is not binary-safe for blob/utf8 edge cases. Acceptable for current schema (no BLOB columns) but should be replaced with `PDO::quote()` in a future maintenance pass.
- **Single shared DB user** (`fonfixco_leng`) has full DDL+DML. Auto-migrations on every request rely on this. Splitting into a low-priv runtime user and a separate migration user is a future improvement.
- **Telegram and CDN reliance** create third-party dependencies. Outages there don't break the POS but mute alerts / break charts respectively.
- **Single-tenant.** No row-level access controls beyond `restrict_branch` + role.

---

## 4. Verification — how to confirm v17.0 is live

```bash
# from any internet-connected machine (no shell needed; use any online curl tool)
curl -i https://system.fon2fix.com.my/config.php           # expect 403
curl -i https://system.fon2fix.com.my/db.php               # expect 403
curl -i https://system.fon2fix.com.my/php.ini              # expect 403
curl -i -A "" https://system.fon2fix.com.my/api.php        # expect 403 (bot block)
curl -i https://system.fon2fix.com.my/uptime_monitor.php   # expect 403 (no token)
curl -I https://system.fon2fix.com.my/ | grep -i 'powered\|hsts\|x-frame'
#  - no X-Powered-By
#  - Strict-Transport-Security present on HTTPS
#  - X-Frame-Options present
```

In the browser, with DevTools open on the dashboard:

- Response headers should include `Content-Security-Policy-Report-Only`, `Permissions-Policy`, `Strict-Transport-Security`.
- Console should show no SRI rejection errors.
- Network tab: every CDN `<script>` request shows `integrity` attribute populated.

---

## 5. Patch responsibility & timelines

| Patch | Operator action required | Deadline after deploy |
|---|---|---|
| P-001 → P-005 | Rotate DB password, backup secret_key, Telegram bot token | Same day |
| P-008 | (optional) lower `security.session_lifetime` | When desired |
| P-009 | Verify `backups/ratelimit/` exists + writable | Same day |
| P-012 | Verify [.htaccess](.htaccess) is honoured by LiteSpeed | Same day |
| P-013 | Browser smoke test of charts + QR scan + PDF | Same day |
| HSTS bump (300 → 31536000) | Edit [db.php](db.php), [index.php](index.php), [api.php](api.php) | After 1 week of stable HTTPS |
| Force-HTTPS in `.htaccess` | Uncomment last block of [.htaccess](.htaccess) | After 1 week + cron audit |

---

## 6. Reporting a vulnerability

For internal staff: contact the Endie account holder directly with a description of the issue, reproduction steps, and (if known) suggested mitigation. Do NOT post details in chat groups or the activity log.

For automated alerts: `security_audit.php` (weekly cron) sends Telegram messages on finding spikes. Investigate within 24 hours.
