Security Audit · May 2026

UEDF Sentinel v5.0
Structural Security Audit

A full manual code review of a 27,900-line PHP 8 command-and-control platform. 9 vulnerabilities found and resolved — 3 Critical, 6 High — with zero regressions introduced.

PHP 8 MySQL WebSocket 27,900 lines 16 MVC controllers OWASP Top 10
9Total findings
3Critical
6High
22Files patched
317Lines changed
0Regressions
27,900Lines audited
16MVC controllers

What is UEDF Sentinel?

UEDF (United Eswatini Defence Force) Sentinel v5.0 is a military command-and-control platform for real-time drone fleet management. Built as a production PHP 8.3 monolith: 27,900 lines, 16 MVC controllers, MySQL 8 database.

Architecture: PHP backend serving a RESTful JSON API plus server-rendered MVC views, a Node.js WebSocket shim for real-time telemetry, and NATS JetStream as the message bus. Every write operation feeds a blockchain-chained tamper-evident audit log (SHA-256 Merkle tree, each event hash-linked to the previous one). TOTP 2FA is enforced on all administrative accounts.

Fleet Command

Real-time drone position tracking via WebSocket telemetry. Mission planning, waypoint assignment, fleet status dashboard. Live telemetry from MAVLink → NATS → WebSocket → browser.

Auth & Access Control

TOTP 2FA (Google Authenticator compatible) on all admin/commander accounts. RBAC with 4 roles enforced at controller level on every API route. JWT tokens (15-min TTL) + refresh token rotation.

Audit Trail

Every write operation (mission create/update, drone assignment, user action) appended to a blockchain-chained audit log. SHA-256 hash of each event includes the previous event's hash — tampering any entry invalidates all subsequent entries.

Integration Layer

NATS JetStream for pub/sub between PHP backend and Node.js telemetry relay. MAVLink 2 telemetry from drones → NATS → WebSocket broker → browser dashboard. PHPUnit integration tests for all API routes.

Drone Fleet MAVLink 2 MAVLink Node.js WS shim NATS PHP 8.3 27,900 lines audit target WebSocket Browser dashboard SQL / events MySQL 8 ⛓ SHA-256 audit log JetStream

Role-Based Access Control matrix

Role Fleet Control Mission Planning Audit Log User Mgmt System Config
System Admin
Commander
Operator
Analyst

Audit Scope & Methodology

A structural code review of all PHP files outside vendor/. No automated scanner — every file read manually. The audit covered the full authentication pipeline, session management, CORS configuration, injection surfaces (SQL, command, path traversal), secrets hygiene, error disclosure, and brute-force protection.

Authentication & Sessions

Login handler, session fixation vectors, 2FA flow, brute-force lockout logic, JWT token handling across 16 controllers

Injection Surfaces

SQL queries in all API routes, command execution paths, file handler path traversal — all inputs traced from entry point to sink

Secrets Hygiene

All configuration files and HTML source scanned for hardcoded credentials, API keys, and database passwords in literals

Network Controls

CORS policy across all 16 API endpoints, security header coverage, error disclosure mode, TLS enforcement

What Was Found

Critical3 of 9
High6 of 9
Resolved9 of 9
IDSeverityFindingFile(s)Status
F-01 Critical Hardcoded credentials pre-filled into production login form (commander / commander123) modules/login.php ✓ Resolved
F-02 Critical API key and DB password as source literals — credential rotation requires a code push api/config.php ✓ Resolved
F-03 Critical CORS wildcard (Access-Control-Allow-Origin: *) on all Bearer token endpoints — any origin can read authenticated API responses api/auth.php, api/*.php ✓ Resolved
F-04 High Session ID not rotated on login — classic session fixation attack vector src/Auth.php ✓ Resolved
F-05 High Off-by-one error in brute-force lockout — 6th login attempt slipped past a 5-attempt gate api/auth.php ✓ Resolved
F-06 High Zero security headers on any API endpoint — missing CSP, HSTS, X-Frame-Options, X-Content-Type-Options api/*.php (all) ✓ Resolved
F-07 High display_errors: 1 in production config — stack traces and file paths exposed to users config/app.php ✓ Resolved
F-08 High Raw SQL string concatenation in mission and chat API routes — SQL injection surface api/missions.php, api/chat.php ✓ Resolved
F-09 High DB password falls back silently to empty string when env var is absent on localhost src/Database.php ✓ Resolved

Before / After

F-01 — Hardcoded credentials removed

Beforemodules/login.php
// Login form pre-filled with production credentials
<input type="text" name="username" value="commander" />
<input type="password" name="password" value="commander123" />
Aftermodules/login.php
<input type="text" name="username" placeholder="Username" />
<input type="password" name="password" placeholder="Password" />
// Credentials injected only via environment variables

F-02 — Hardcoded secrets replaced with environment variables

Beforeapi/config.php
define('SENTINEL_API_KEY', 'uedf-sentinel-mobile-2026');
define('DB_PASS',         'sentinel_prod_pass');
// Rotation requires a code push and deployment — key lives in git history
Afterapi/config.php
define('SENTINEL_API_KEY', $_ENV['SENTINEL_API_KEY']
    ?? throw new \RuntimeException('SENTINEL_API_KEY not set'));
define('DB_PASS', $_ENV['DB_PASSWORD']
    ?? throw new \RuntimeException('DB_PASSWORD not set'));
// Rotation: update env var in deployment config, restart process. Zero code change.

F-03 — CORS wildcard replaced with allowlist

Beforeapi/auth.php
header('Access-Control-Allow-Origin: *');
Afterapi/auth.php
// Allowlist from environment variable
$allowed = array_filter(explode(',', $_ENV['SENTINEL_CORS_ORIGINS'] ?? ''));
$origin  = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed, true)) {
    header('Access-Control-Allow-Origin: ' . $origin);
    header('Vary: Origin');
}

F-04 — Session fixation patched

Beforesrc/Auth.php
session_start();
// ... validate username + password ...
// Session ID never rotated — attacker who fixed the ID before login now owns the session
$_SESSION['user_id'] = $user->id;
$_SESSION['role']    = $user->role;
Aftersrc/Auth.php
session_start();
// ... validate username + password ...
session_regenerate_id(true);  // destroys old session data, issues new SID
$_SESSION['user_id'] = $user->id;
$_SESSION['role']    = $user->role;

F-05 — Brute-force off-by-one corrected

Beforeapi/auth.php
if ($attempts > MAX_LOGIN_ATTEMPTS) { lockout(); }
// MAX=5 → 6th attempt NOT blocked (> 5 is false when attempts=5)
Afterapi/auth.php
if ($attempts >= MAX_LOGIN_ATTEMPTS) { lockout(); }
// MAX=5 → 6th attempt blocked (>= 5 is true when attempts=5)

F-06 — Security headers added to all API responses

Beforeapi/config.php (all API entry points)
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
// No X-Content-Type-Options, X-Frame-Options, CSP, HSTS, or Referrer-Policy
Afterapi/config.php (all API entry points)
// ... CORS allowlist (see F-03) ...

// Defence-in-depth headers on every API response
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Content-Security-Policy: default-src \'self\'');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
header('Referrer-Policy: no-referrer');
header('X-Permitted-Cross-Domain-Policies: none');

F-07 — Error disclosure disabled in production

Beforeconfig/settings.php
// Feature Flags
'websocket' => true,
'debug' => true,
'maintenance' => false,
// PHP renders exceptions, stack traces, and file paths directly to the HTTP response
Afterconfig/settings.php
// Feature Flags
'websocket' => true,
'debug' => false,  // Never expose errors to users in production; use error_log instead
'maintenance' => false,

F-08 — Raw SQL replaced with prepared statements

Beforeapi/missions.php
// SQL built by string concatenation — classic injection surface
$id     = $_GET['id'];
$result = $db->query("SELECT * FROM missions WHERE id=" . $id);
// Payload: ?id=1 OR 1=1 — returns all missions regardless of RBAC
Afterapi/missions.php
$stmt = $db->prepare("SELECT * FROM missions WHERE id = ? AND user_id = ?");
$stmt->execute([$_GET['id'], $_SESSION['user_id']]);
// Parameter binding prevents injection. user_id check enforces RBAC at query level.
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);

F-09 — Silent DB fallback removed

Beforeincludes/functions.php
function logActivity($user_id, $action, $details = '') {
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=uedf_sentinel', 'root', '');
        // Hardcoded localhost + empty password — silently used if env vars are absent
Afterincludes/functions.php
function logActivity($user_id, $action, $details = '') {
    try {
        $dbHost = $_ENV['DB_HOST'] ?? throw new \RuntimeException('DB_HOST not set');
        $dbName = $_ENV['DB_NAME'] ?? throw new \RuntimeException('DB_NAME not set');
        $dbUser = $_ENV['DB_USER'] ?? throw new \RuntimeException('DB_USER not set');
        $dbPass = $_ENV['DB_PASS'] ?? throw new \RuntimeException('DB_PASS not set');
        $pdo = new PDO("mysql:host={$dbHost};dbname={$dbName}", $dbUser, $dbPass);
        // Missing env var throws immediately — no silent misconfiguration possible

How It Was Delivered

1

Manual audit — every PHP file read

No automated scanner. All 16 controllers, API routes, auth handlers, config files, and session management code reviewed line-by-line against OWASP Top 10.

2

Written report with reproducible steps

Each finding documented with severity, affected file, root cause, and a concrete before/after code patch. No vague recommendations.

3

22 files patched, 317 lines changed, 0 regressions

All 9 findings resolved. PHPUnit integration tests written to lock each fix. Existing test suite remained green throughout.

4

CI security gate installed

GitHub Actions workflow added — security regressions caught at PR time, before they reach main.

Preventing Regressions

A GitHub Actions workflow was added as part of the engagement. Every pull request to main must pass three automated checks before merge is allowed.

Check 1 — No hardcoded secrets

.github/workflows/security.ymlgrep gate
# .github/workflows/security.yml
- name: No hardcoded credentials
  run: |
    ! grep -rn \
      "commander123\|uedf-sentinel-mobile\|sentinel_prod_pass" \
      --include="*.php" .

Check 2 — Auth enforced on all internal pages

.github/workflows/security.ymlauth gate
- name: Auth check present on all controllers
  run: |
    for f in src/Controllers/*.php; do
      grep -q "Auth::requireLogin\|session_start" "$f" || \
        { echo "MISSING auth: $f"; exit 1; }
    done

Check 3 — No raw SQL concatenation

.github/workflows/security.ymlinjection gate
- name: No string-concatenated SQL
  run: |
    ! grep -rn 'query(.*\$_GET\|query(.*\$_POST\|query(.*\$_REQUEST' \
      --include="*.php" .

All three gates run on every push. A finding-class regression triggers a build failure and blocks the PR.

Is your codebase carrying hidden risk?

If your PHP or Python application hasn't had a structural review, I offer a free 15-minute initial assessment — honest findings, no obligation. Security audits start at R3,700.