GHOSTPORT OS
PATCH & FEATURE LOG
Published: March 23, 2026 • Covering: Pre-Audit + Rounds 1–10 + Theme Engine + Fleet Integration + Family Shield
Over the course of seven days (March 17–23, 2026), we conducted a comprehensive multi-round security audit of every component in GhostPort OS — from the Express API server and frontend SPA to nftables firewall profiles, shell scripts, and systemd services. This log documents 225 bugs found, 96 patched across 11 audit rounds, a brand new color theming engine with RGB breathing mode, Family Shield parental controls with DNS + IP-level blocking, and the Phase 2 fleet integration system with Stripe subscriptions.
Pre-Audit Fixes — March 17, 2026
Scripts: fix-permissions.py, fix-restore-btn.py, fix-security-bugs.py, fix-round2.py, fix-mode-and-nft.sh, fix-dns-upstream.py, fix-pihole-cmd.py, fix-arsenal-race.py, fix-install-banner.py
| # | Severity | File | Bug | Fix |
| 1 | MED | public/index.html | Restore button missing from repair panel | Added backup restore UI button |
| 2 | HIGH | ghostport-server.js | File permissions too open on config files | Enforced 0600 on auth.json, pihole.json, arsenal.json |
| 3 | CRIT | ghostport-server.js | WireGuard config accepts PostUp/PostDown directives — arbitrary root command execution | Strip dangerous directives (PostUp, PostDown, PreUp, PreDown, SaveConfig) on upload |
| 4 | HIGH | ghostport-server.js | Hostapd SSID/password not validated — newline injection into config file | Added regex validation for SSID (1-32 chars, no newlines) and password (8-63 printable ASCII) |
| 5 | HIGH | ghostport-server.js | Schedule deletion command injection via unsanitized ID in sed command | Added regex validation: schedule IDs must match /^[a-z0-9]+$/ |
| 6 | HIGH | ghostport-server.js | Missing input validation on mode switch — arbitrary string passed to shell | Whitelist validation: mode must be one of isp, zerotrust, doublehop, zhop |
| 7 | MED | ghostport-server.js | Hostapd config read via shell cat — unnecessary shell invocation | Switched to fs.readFileSync with sudo fallback |
| 8 | CRIT | gp-mode / nftables | DNS leak monitor nftables rules used invalid mixed tcp/udp syntax | Split into separate accept+drop rules matching kill switch pattern |
| 9 | CRIT | gp-mode / gp-dns-upstream | DNS upstream switch called before mode file update — reads OLD mode, applies wrong DNS | Moved gp-dns-upstream call to after modefile write in every mode case |
| 10 | MED | ghostport-server.js | Pi-hole API called via deprecated CLI syntax | Updated to Pi-hole v6 REST API |
| 11 | HIGH | ghostport-server.js | Arsenal config race condition — concurrent read-modify-write on kill switch, MAC, QUIC toggles | Introduced withArsenal(fn) mutex pattern for atomic operations |
| 12 | MED | ghostport-server.js | Arsenal.json not created on first boot — all toggles fail with ENOENT | Added default file creation with correct ownership on startup |
Round 1 — March 18, 2026
Script: fix-bugs-march18.py • 13 patches applied
| # | Severity | File | Bug | Fix |
| 13 | CRIT | gp-mode | DNS upstream called inside apply_profile() before modefile update — wrong DNS server applied | Moved to post-modefile write for all 4 modes |
| 14 | HIGH | ghostport-server.js | Kill switch toggle uses raw readArsenal/writeArsenal — race condition | Wrapped in withArsenal() mutex |
| 15 | HIGH | ghostport-server.js | MAC randomization toggle uses raw readArsenal/writeArsenal | Wrapped in withArsenal() mutex |
| 16 | HIGH | ghostport-server.js | QUIC block toggle uses raw readArsenal/writeArsenal | Wrapped in withArsenal() mutex |
| 17 | MED | ghostport-server.js | Hostapd config read via sudo cat shell command | Changed to fs.readFileSync with graceful fallback |
| 18 | MED | ghostport-server.js | Backup version missing from export metadata | Added version: "1.0" to backup JSON |
Round 2 — March 18, 2026
Scripts: fix-bugs-round2.py, fix-ads-tally.py, fix-pwa-sw.py, fix-install-route.py • XSS, session, and feature fixes
| # | Severity | File | Bug | Fix |
| 19 | HIGH | public/index.html | Log function XSS — msg inserted into innerHTML without escaping | Added HTML entity escaping before interpolation |
| 20 | HIGH | public/index.html | Diagnostics output XSS — c.name, c.detail, c.fix unescaped in innerHTML | Added esc() helper function, escaped all diagnostic fields |
| 21 | HIGH | public/arsenal.js | Security scan output XSS — warning/suggestion id, message, fix unescaped | Wrapped all fields in escapeHtml() |
| 22 | MED | ghostport-server.js | Ads tally file not created on startup — privacy-preserving counter fails | Created /etc/ghostport/ads-tally.json with correct ownership |
| 23 | MED | ghostport-server.js | Sessions never pruned — memory leak over time | Added pruning interval every 10 minutes, removes expired sessions |
| 24 | MED | public/sw.js | Service worker was self-destruct script — PWA not installable | Replaced with minimal network-only SW with fetch handler for Chrome installability |
| 25 | LOW | ghostport-server.js | No public route for install page | Added /install.html, /install, and /qr routes before auth middleware |
Round 3 — March 18, 2026
Script: fix-bugs-round3.py • 6 bugs, 6 patches
| # | Severity | File | Bug | Fix |
| 26 | HIGH | ghostport-server.js | Schedule add uses raw readArsenal/writeArsenal — race condition with other Arsenal ops | Wrapped in withArsenal() mutex |
| 27 | HIGH | ghostport-server.js | Schedule delete uses raw readArsenal/writeArsenal | Wrapped in withArsenal() mutex |
| 28 | LOW | ghostport-server.js | Temp file /tmp/gp-sched-line never cleaned up after cron creation | Added fs.unlinkSync after tee |
| 29 | MED | ghostport-server.js | Factory reset incomplete — doesn't clear ads tally, cron schedules, or temp files | Added cleanup for ads-tally.json, ghostport-schedules, and temp files |
| 30 | HIGH | gp-mode-boot | No fallback if saved mode fails on boot — device stuck with broken firewall | Added 30-second timeout + automatic ISP fallback on failure |
| 31 | MED | ghostport-server.js | req.ip spoofable via X-Forwarded-For header | Set app.set("trust proxy", false) |
Round 4 — March 18, 2026
Script: fix-bugs-round4.py • 9 bugs, 12 patches
| # | Severity | File | Bug | Fix |
| 32 | CRIT | ghostport-server.js | withArsenal() mutex deadlock — thrown error breaks promise chain permanently, all future Arsenal operations hang | Error handler catches and logs without re-throwing — chain stays alive |
| 33 | HIGH | ghostport-server.js | stopKillSwitch() reads mode from file and passes to exec() without validation — command injection via compromised modefile | Added whitelist validation before execution |
| 34 | HIGH | ghostport-server.js | startKillSwitch() recovery path same issue | Added whitelist validation |
| 35 | HIGH | ghostport-server.js | WireGuard stats: parseInt(rx || 0) returns NaN on corrupted data; no array bounds check on tab-split output | Added parseInt(x, 10) || 0 fallback and filter(line => parts.length >= 7) |
| 36 | MED | ghostport-server.js | Restore endpoint: fs.unlinkSync(tmpWg) crashes on permission error | Wrapped in try-catch |
| 37 | MED | ghostport-server.js | Restore endpoint: fs.unlinkSync(tmpHap) same issue | Wrapped in try-catch |
| 38 | MED | ghostport-server.js | Kill switch: concurrent toggle + monitor causes state mismatch if interval becomes stale | Clears stale interval before starting new one |
| 39 | MED | ghostport-server.js | Schedule ID collision — Date.now().toString(36) same millisecond = same ID | Appended 4 random hex chars via crypto.randomBytes(2) |
| 40 | MED | ghostport-server.js | Session TTL fixed at 24h — active user kicked at expiry even during use | Sliding window: session.created = Date.now() on each valid request |
| 41 | LOW | ghostport-server.js | ISP IP cache never expires — DNS leak detection uses stale data if ISP changes | Added 1-hour cache expiry with ispIpCacheTime timestamp |
Round 5 — March 18, 2026
Script: fix-bugs-round5.py • 7 bugs, 10 patches
| # | Severity | File | Bug | Fix |
| 42 | CRIT | ghostport-server.js | ispIpCacheTime used but never declared — DNS leak monitor crashes with ReferenceError on first run | Added let ispIpCacheTime = 0 declaration |
| 43 | HIGH | ghostport-server.js | No rate limit on /api/tools/speedtest — DoS vector via repeated 60-120 second system commands | Added 2-minute cooldown between speed tests |
| 44 | HIGH | ghostport-server.js | Lynis scan flag lynisScanRunning not reset on exception — one crash permanently locks scanning | Moved reset to finally block |
| 45 | MED | ghostport-server.js | Trouble ticket description/contact accept newlines — webhook message injection | Strip \r\n, cap description at 500 chars, contact at 100 |
| 46 | MED | ghostport-server.js | Error responses leak internal paths, stderr, and webhook URLs | Sanitized all error messages to generic strings |
| 47 | LOW | ghostport-server.js | /login/ with trailing slash returns 404 | Changed route to regex: /^\/login\/?$/ |
| 48 | LOW | ghostport-server.js | /install/ with trailing slash returns 404 | Changed route to regex: /^\/install\/?$/ |
Round 5b — March 19, 2026
Script: fix-bugs-round5b.py • 9 bugs across 5 files • Deep audit of previously unaudited files
| # | Severity | File | Bug | Fix |
| 49 | CRIT | public/pwa.html | XSS in setup summary — user-supplied name and email interpolated into innerHTML unescaped | Added esc() helper, escaped name, serial, and email fields |
| 50 | CRIT | public/pwa.html | XSS in log rendering — l.msg inserted into innerHTML without escaping | Wrapped in esc() |
| 51 | HIGH | public/da.html | XSS in QA error handler — e.message rendered as raw HTML | Added esc() helper, escaped error message |
| 52 | HIGH | public/da.html | XSS in log entries — l.message and l.timestamp unescaped | Escaped both fields |
| 53 | HIGH | public/da.html | XSS in device info — all API-sourced device fields rendered as raw HTML | Escaped labels and values |
| 54 | HIGH | public/da.html | XSS in QA checklist — c.name and c.detail unescaped in innerHTML | Escaped both fields |
| 55 | HIGH | isp.nft | Missing IPv6 drop in forward chain — clients can bypass routing via IPv6 tunnels | Added meta nfproto ipv6 counter drop to forward chain |
| 56 | HIGH | zerotrust.nft | Missing IPv6 drop in forward chain — DNS lock bypass via IPv6 | Added meta nfproto ipv6 counter drop to forward chain |
| 56b | MED | gp-dns-upstream | Silent failures — no error reporting if config file missing or sed replacement fails | Added file existence check and post-sed verification with error output |
New Feature — Theme Color Engine
🎨 Infinite Color Picker + RGB Breathing Mode
The GhostPort Command Deck now supports full UI color customization. Every element — accents, backgrounds, borders, text, glows — adapts to your chosen color in real time.
- 8 Preset Swatches — Green (default), Purple, Blue, Red, White, Cyan, Amber, and Gunmetal. One-click to apply.
- Custom Color Picker — Pick literally any color. The entire UI derives backgrounds, borders, text, and glow values from your chosen hue using a real-time HSL engine.
- RGB Breathing Mode — Smooth, continuous hue rotation powered by
requestAnimationFrame. A sine-wave brightness pulse creates a living, breathing effect. Full spectrum rotation every ~6.5 seconds at 30fps.
- Achromatic Support — Grey, white, and black colors are handled correctly. The engine passes saturation through to prevent unwanted red tinting on neutral colors.
- Persistence — Your choice saves to
localStorage and applies across both the login screen and dashboard. Survives page reloads and browser restarts.
- Login Screen Sync — The authentication page matches your chosen theme, including RGB breathing mode. First impression matches the rest of the UI.
- Migration — Users with older named theme values in localStorage are automatically migrated to the new hex-based system.
Round 6 — March 21, 2026
Script: fix-bugs-round6.py • 9 patches across server & firewall
| # | Severity | File | Bug | Fix |
| 57 |
HIGH |
ghostport-server.js |
Schedule deletion endpoint had copy-pasted finally block from Lynis scanner — resetting unrelated lynisScanRunning flag and returning wrong error message ("Security scan failed") |
Removed wrong finally block, fixed error message to "Failed to delete schedule" |
| 58 |
HIGH |
ghostport-server.js |
6 locations used predictable temp file names in /tmp/ (e.g. /tmp/gp-hostapd.conf) — symlink attack vector on world-writable directory |
Added gpTmpFile() helper using crypto.randomBytes(8) for unique names. Applied to hostapd, WireGuard, restore, MAC randomization, and schedule writes |
| 59 |
MED |
ghostport-server.js |
Restore endpoint exposed e.message in error response — could leak filesystem paths or command output |
Replaced with generic "Config restore failed" message, logs details server-side |
| 60 |
MED |
ghostport-server.js |
DNS leak check used curl -4 (IPv4 only) — wouldn't detect IPv6 DNS leaks |
Switched to dual-stack curl -s to catch both v4 and v6 leak types |
| 61 |
LOW |
ghostport-server.js |
Gateway and speedtest ping commands passed IPs without shell quoting |
Added single-quote wrapping around interpolated IP variables for defense-in-depth |
| 62 |
MED |
ghostport-server.js |
Restore endpoint used systemctl restart wg-quick@wg0 but gp-mode manages wg0 directly via ip/wg commands — service conflicts with existing interface |
Replaced with gp-mode reapply for consistency with how WireGuard is actually managed |
| 63 |
MED |
ghostport-server.js |
Kill switch nftables rules only allowed port 4200 (HTTP dashboard) but not 4201 (HTTPS) — would lose dashboard access over HTTPS during kill switch activation |
Added port 4201 to kill switch accept rules |
| 64 |
LOW |
public/index.html |
Three CSS classes (.mode-desc, .log-msg.info, .arsenal-desc) used hardcoded green hex values that didn't respond to theme color switching |
Replaced #6a9a6a and #5a8a5a with var(--text-dim) |
| 65 |
LOW |
common.nft |
Ports 80 and 443 had accept rules open to all interfaces — redundant since policy is accept but misleading |
Already cleaned up (found pre-fixed during verification) |
Round 7 — March 21, 2026
Script: fix-bugs-round7.py • 18 patches across 4 categories
| # | Severity | File | Bug | Fix |
| 66–80 |
MED |
ghostport-server.js |
15 endpoints exposed raw e.message in error responses. Child process errors could leak filesystem paths, command names, or internal system details to authenticated clients |
All error responses now return generic messages ("Operation failed", "Mode switch failed", etc.). Full details logged server-side with console.error() |
| 81 |
HIGH |
ghostport-server.js |
DHCP lease hostname from dnsmasq not HTML-escaped in /api/arsenal/clients response. A device with <script> in its hostname could inject into the client list UI |
Wrapped hostname in escapeHtml() |
| 82 |
MED |
ghostport-server.js |
POST /api/arsenal/dnstest had no rate limiting. Could be spammed to flood external DNS lookups and ifconfig.me requests, causing resource exhaustion |
Added 30-second cooldown (same pattern as speedtest's 2-minute throttle) |
| 83 |
HIGH |
ghostport-server.js |
Restore endpoint wrote directly to /etc/ghostport/arsenal.json without the withArsenal() mutex. Concurrent arsenal operations (toggle, schedule add) could corrupt the config file |
Wrapped in await withArsenal() to serialize writes |
Bug Fix — March 21, 2026
Post-Round 7 hotfix • Server crash-loop recovery
| # | Severity | File | Bug | Fix |
| 84 |
CRIT |
ghostport-server.js |
Duplicate try{} block in flushTally() from a prior round patch caused a SyntaxError on startup — server entered a crash-loop with 162 restarts before detection |
Collapsed duplicate try blocks into a single try/catch/finally structure |
Phase 2 — Fleet Integration (March 21, 2026)
Heartbeat Agent (gp-heartbeat)
Systemd timer-driven agent that checks in with the EC2 fleet server every 5 minutes. Each heartbeat sends the device’s firmware version, uptime, and current mode. The fleet server responds with subscription status and any pending commands (e.g., mode change, config update).
- 5-minute cadence — systemd timer triggers
gp-heartbeat script on a reliable interval
- Payload — firmware version, system uptime, current firewall mode, device serial
- Response — subscription tier + expiry, pending remote commands, firmware update availability
Install Page (/install)
Customer-facing dark-themed activation page served at /install. Users enter their license key to activate the device. The page auto-provisions the router without requiring prior authentication.
- Dark-themed UI — matches the GhostPort Command Deck aesthetic
- License key input — validates format client-side, submits to fleet activation endpoint
- Auto-provisioning — on success, generates credentials and completes device setup
/api/fleet/activate Endpoint
No-auth device setup endpoint. Accepts a license key, registers the device with the fleet server, and returns freshly generated credentials for the customer.
- No authentication required — designed for first-boot activation before any passcode exists
- Auto-generates — dashboard passcode, WiFi AP password, and Pi-hole admin password
- Fleet registration — sends device serial + license key to EC2 fleet API, receives subscription tier
gp-new --auto
Non-interactive factory provisioning mode for the gp-new registration script. Enables headless device setup during manufacturing or bulk deployment without manual prompts.
- Non-interactive — all prompts bypassed with sane defaults or pre-configured values
- Factory use case — flash, boot, auto-register, ship
Stripe Subscription Integration
Three-tier subscription model with webhook-driven license management. Stripe events flow to the EC2 fleet server, which updates each device’s subscription status. The Pi’s heartbeat agent picks up changes on its next check-in.
- Basic — $5/mo — Core privacy routing, ISP + ZeroTrust modes
- Phantom Guardian — $10/mo — All modes including DoubleHop VPN, kill switch, MAC randomization
- Unholy Covenant — $15/mo — Full feature set: ZHop, scheduled mode switching, priority support
EC2 Fleet API Pipeline
End-to-end subscription lifecycle: Stripe webhook fires on payment events → EC2 fleet server updates the device record → Pi heartbeat picks up the new subscription status on next 5-minute check-in.
- Stripe → Fleet — Webhook receiver on EC2 validates Stripe signatures, maps customer to device
- Fleet → Pi — Heartbeat response includes updated subscription tier, expiry, and feature flags
- Grace period — failed payments trigger a grace window before feature downgrade
New Feature — Family Shield (March 22, 2026)
🛡 Parental Controls with DNS + IP Blocking
Family Shield is a dual-layer parental control system that combines Pi-hole DNS filtering with nftables IP-range firewall rules. DNS blocking alone can’t stop apps like TikTok that use CDN fallbacks and hardcoded IPs — so we added firewall rules that block entire ASN-owned IP ranges at the packet level.
- 5 Categories — Adult Content, Gambling, Meta Apps (FB/IG/WhatsApp), TikTok, X (formerly Twitter)
- DNS Blocking — Pi-hole v6 group-based filtering with 22,459+ domain blocklists per category
- IP-Range Blocking — nftables sets blocking ByteDance (13 CIDR ranges), Meta (16 ranges), and X/Twitter (6 ranges) at the packet level
- Per-Device Shielding — Shield specific devices without blocking yourself. DISCOVER button scans for connected clients
- Master Toggle — One switch enables/disables all parental controls. Category toggles for granular control
- Persistent Config — Settings saved to
/etc/ghostport/family-shield.json, IP blocks restored on server restart
Family Shield — Bugs Fixed During Development
| # | Severity | Component | Bug | Fix |
| 85 | HIGH | Server | Pi-hole v6 _suggestions API returns array format, not object — device discovery and shielding broken | Rewrote parsing for {clients: [{addresses, names}]} format in 3 endpoints |
| 86 | HIGH | Server | Pi-hole v6 lists API requires ?type=block as query parameter, not in JSON body | Added query parameter to all POST/PUT list endpoint URLs |
| 87 | MED | Frontend | Toggle race condition — load() polling overwrites toggle state during API calls | Added fs.busy flag, load() skips when busy |
| 88 | MED | Config | family-shield.json owned by root, server runs as ghostport-admin | Fixed ownership with chown ghostport-admin:ghostport-admin |
| 89 | MED | Pi-hole | Session exhaustion — default max 16 sessions burned by debug restarts | Bumped to 64 via pihole-FTL --config webserver.api.max_sessions 64 |
| 90 | HIGH | Pi-hole | Gravity database corruption from repeated FTL restarts — tables missing | Rebuilt gravity.db from scratch, re-added blocklists and groups |
| 91 | MED | Blocking | TikTok DNS blocking ineffective — app uses Akamai CDN fallbacks | Added nftables IP-range blocking for ByteDance ASN ranges |
Round 11 — March 23, 2026
3 parallel audit agents • 68 bugs found across server, frontend, firewall, and system scripts • Automated pipeline deployment
Critical (5)
| # | File | Bug |
| 158 | CRIT server.js | /api/fleet/activate unauthenticated — attacker can set all credentials and take over device before legitimate activation |
| 159 | CRIT server.js | /api/fleet/activate race condition — concurrent requests both pass “already activated” check, overwriting credentials |
| 160 | CRIT gp-new | Hardcoded fleet API bearer token in plaintext — anyone with script read access can impersonate fleet registrations |
| 161 | CRIT gp-blog-deploy | StrictHostKeyChecking=no on SSH disables host key verification — enables MITM on deployment pipeline |
| 162 | CRIT gp-passcode | Command injection via AUTH_FILE path interpolated unescaped into python3/node eval strings |
High (14)
| # | File | Bug |
| 163 | HIGH server.js | Session sliding window resets 24h TTL on every request — sessions effectively never expire (7-day absolute limit only) |
| 164 | HIGH server.js | /api/auth/check and session middleware have divergent TTL behavior |
| 165 | HIGH server.js | CSP allows unsafe-inline for script-src and style-src — enables XSS through inline injection |
| 166 | HIGH server.js | HTTP port 4200 sends session cookies without Secure flag — cleartext over LAN |
| 167–168 | HIGH common.nft | Management chain accepts ALL traffic from LAN_IF/TS_IF at priority -100 — mode-specific input restrictions never evaluated |
| 169–170 | HIGH isp/zerotrust.nft | Forward chain policy is accept instead of drop — unmatched forwarded traffic passes through |
| 171 | HIGH zerotrust.nft | DoH block only covers 7 resolver IPs — hundreds of providers not blocked |
| 172 | HIGH doublehop.nft | No DNS leak protection in output chain — Pi can send cleartext DNS out eth0 bypassing WG tunnel |
| 173 | HIGH gp-mode | Rollback subshell race — may fork gp-mode before PID killed, causing concurrent nftables modifications |
| 174 | HIGH gp-mode | mktemp WireGuard config world-readable — briefly exposes WG private keys in /tmp |
| 175 | HIGH arsenal.js | Schedule delete onclick — escapeHtml doesn’t escape backslash, potential JS string breakout |
Medium (27) & Low (22)
| # | Sev | Category | Description |
| 176 | MED | Security | /api/tools/backup exposes WiFi passphrase to authenticated users |
| 177 | MED | RaceCondition | withArsenal mutex swallows errors, stale data in subsequent calls |
| 178 | MED | Validation | WireGuard PostUp/PostDown strip is case-insensitive but wg-quick is case-sensitive |
| 179 | MED | Security | /api/tools/restore accepts hostapd config with minimal validation |
| 180 | MED | Auth | Lockout by req.ip — NAT shared IP locks out all LAN users |
| 181 | MED | RaceCondition | Concurrent mode switch requests read same previousMode |
| 182 | MED | Security | /api/ticket sends unsanitized description to Discord webhook |
| 183 | MED | Firewall | No DoQ/DoH blocking to unlisted servers beyond 7 IPs |
| 184–185 | MED | DNS | gp-dns-upstream sed not idempotent; wildcard catches invalid modes silently |
| 186–187 | MED | Logic | gp-dns-switch references unbound (inactive); no root check |
| 188 | MED | RaceCondition | gp-mode has no mutex/lockfile for concurrent invocations |
| 189 | MED | Security | nftables backup world-readable in /tmp |
| 190 | MED | DNS | gp-dns-watchdog restarts wg-quick@wg0 conflicting with gp-mode manual setup |
| 191 | MED | Security | gp-blog-generate interpolates results into HTML without escaping |
| 192–198 | MED | Logic/UI | Toggle desync on rapid clicks, stacked arm timers, missing res.ok checks, no domain/WG validation, hardcoded Pi-hole status & DNS resolver label |
| 199–202 | MED | Security | Backup import no schema validation, logo leaks referrer (missing noreferrer), WiFi password in stdout |
| 203–225 | LOW | Various | SSL cert never reloaded, run() errors ignored, double-sanitized password, login attempt count leak, unlimited sessions/schedules, fsIpBlock fire-and-forget, IPv6 input unfiltered, rollback PID writable, QUIC self-traffic, passcode show/reset mismatch, blog round detection, password complexity, sed without sudo, innerHTML race, DNS alert placement, WiFi charset, window.close blocked, polling overlap, no ARIA/a11y, 401 redirect bug, hardcoded version |
Round 10 — March 23, 2026
3 parallel audit agents • 82 bugs found across server, frontend, and system scripts • 21 patches applied
Critical (11)
| # | File | Bug | Fix |
| 92–100 | CRIT arsenal.js | 9 XSS vulnerabilities — data.error, data.status, and speed test values injected unsanitized into innerHTML across DNS Leak, Ping, IP Leak, Speed Test, System Update, Blocked Domains, and Security Scan results | All values wrapped in escapeHtml() |
| 101 | CRIT server.js | Hardcoded fleet token fallback — anyone reading source can register rogue devices | Removed fallback, fleet.json required |
| 102 | CRIT gp-mode | DoubleHop/ZHop applies nftables forward-drop before wg0 exists — connectivity blackout, full LAN outage if WireGuard fails | Reordered: start_wg0 now runs before apply_profile |
High (10)
| # | File | Bug | Fix |
| 103 | HIGH server.js:90 | Family Shield IP blocks silently fail to restore on startup — FAMILY_SHIELD_IP_RANGES const in temporal dead zone when called | Moved startup restore code after all const declarations |
| 104 | HIGH server.js | No CSRF protection on any POST endpoint | Noted for next round — requires token infrastructure |
| 105 | HIGH server.js | Backup endpoint exposes WiFi passphrase in plaintext JSON | Noted — add passphrase stripping |
| 106 | HIGH common.nft | SSH (port 22) open on ALL interfaces including WAN (eth0) | Restricted to $LAN_IF and $TS_IF only |
| 107 | HIGH gp-passcode | show command broken — auth.json stores hash+salt, not plaintext after reset | Noted — remove show or store encrypted copy |
| 108 | HIGH index.html | esc() doesn't escape single quotes — XSS risk in single-quoted onclick attributes | Added .replace(/'/g,"'") |
| 109 | HIGH arsenal.js | escapeHtml() same single-quote gap | Added .replace(/'/g,"'") |
| 110 | HIGH arsenal.js | Fetch wrapper 401 redirect can loop — Request.toString() returns [object Request] | Noted — fix URL check |
| 111 | HIGH index.html | Google Fonts privacy leak — dashboard phones home to Google on every load | Replaced with system fonts (Courier New, Georgia) |
| 112 | HIGH server.js | hostapd POST reads config without sudo — fails on root-owned files | Added sudo cat to match GET handler |
Medium (27) & Low (14)
41 additional issues found across session handling (sliding TTL, 60s lockout, weak passcode policy), null pointer risks in arsenal.js (6 elements), DNS watchdog restarting wrong service in tunnel modes, common.nft priority override, incomplete DoH/DoT blocking, Family Shield race conditions, and various minor issues (duplicate CSS, hardcoded Mullvad DNS, missing aria labels).
- DNS Watchdog — Now mode-aware: restarts WireGuard in doublehop/zhop, cloudflared in isp/zerotrust
- gp-mode rollback — Fixed
$0 path resolution: now uses full /usr/local/bin/gp-mode path
- Pi-hole status — Hardcoded “UP” dot in UI never updates (noted)
- Tailscale toggle — UI allows stopping Tailscale despite “never stop” safety rule (noted)
- Reboot UX — No reconnection logic after reboot — shows “REBOOTING...” forever (noted)
Cumulative Security Posture
By Severity (All 10 Rounds)
| Severity | Count | Examples |
| CRIT | 22 | Command injection via WireGuard config, DNS upstream timing, mutex deadlock, undeclared variable crash, XSS (setup flow, arsenal errors, speed test), flushTally crash-loop, hardcoded fleet token, doublehop ordering bug |
| HIGH | 42 | Arsenal race conditions, XSS across 5+ HTML files, IPv6 firewall bypass, mode string injection, DoS via speedtest, DHCP hostname XSS, SSH exposed on WAN, IP block startup failure, Google Fonts privacy leak, single-quote escape gap |
| MED | 59 | Error info leaks, trust proxy spoofing, session TTL, factory reset incomplete, webhook injection, DNS test rate limit, Pi-hole v6 API format, toggle race conditions, DNS watchdog mode-unaware, null pointer risks |
| LOW | 22 | Temp file cleanup, trailing slash 404s, ISP IP cache staleness, hardcoded CSS colors, shell quoting, duplicate CSS, missing aria labels, hardcoded Mullvad DNS |
By Category (All 10 Rounds)
| Category | Count | Examples |
| Cross-Site Scripting (XSS) | 28 | Log output, diagnostics, scan results, hostname, setup summary, device info, QA checklist, arsenal error responses (9), single-quote escape gaps (2) |
| Information Disclosure | 20 | Error message leaks (15), webhook URL exposure, hardcoded fleet token, WiFi passphrase in backups, Google Fonts privacy leak, internal path leaks |
| Race Conditions | 11 | Arsenal mutex, restore serialization, schedule ID collision, mutex deadlock fix, Family Shield toggle race, activation TOCTOU |
| Firewall / Network Bypass | 11 | IPv6 tunnel leak, kill switch HTTPS port, DNS prerouting, SSH on WAN, doublehop ordering, incomplete DoH/DoT blocking |
| Command / Shell Injection | 8 | WireGuard PostUp, sed injection, shell interpolation quoting, mode string injection |
| Input Validation | 10 | Mode whitelist, SSID control chars, parseInt NaN fallback, ticket sanitization, weak passcode policy, boolean type coercion |
| Symlink / Temp File | 7 | Predictable /tmp/gp-* names → crypto.randomBytes, WG key in temp file |
| Resource Exhaustion | 5 | Speedtest rate limit, DNS test throttle, session pruning, Lynis lock recovery, no rate limit on auth endpoints |
| Startup / Boot | 4 | IP block TDZ failure, DNS watchdog mode-unaware, rollback path resolution, boot service ordering |
| Crash / Syntax Error | 1 | Duplicate try{} block in flushTally() causing SyntaxError crash-loop (162 restarts) |
Files Modified
| File | Patches | Description |
ghostport-server.js | 61 | Express API server + Family Shield endpoints + IP blocking |
public/index.html | 12 | Main frontend SPA + Family Shield UI + system fonts |
public/arsenal.js | 20 | Security tools module + XSS fixes |
public/pwa.html | 3 | PWA onboarding |
public/da.html | 5 | Device admin panel |
public/sw.js | 1 | Service worker rewrite |
gp-mode | 7 | Mode switching + doublehop ordering fix + rollback path fix |
gp-mode-boot | 1 | Boot restore script |
gp-dns-upstream | 2 | DNS upstream switcher |
gp-dns-watchdog | — | Mode-aware DNS watchdog cron (new) |
isp.nft | 1 | ISP firewall profile |
zerotrust.nft | 1 | ZeroTrust firewall profile |
common.nft | 2 | Shared firewall rules + SSH WAN restriction |
gp-heartbeat | — | Fleet heartbeat agent (new) |
gp-new | — | Fleet registration script (updated) |
public/install.html | — | Customer activation page (new) |
gp-fs-ipblock | — | Family Shield IP blocker (new, integrated into server) |
What’s Next
Fleet Activation — Shipped in Phase 2
Stripe Integration — Shipped in Phase 2
Heartbeat Agent — Shipped in Phase 2
Family Shield — Shipped March 22 — DNS + IP blocking parental controls
SSH WAN Hardening — Fixed March 23 — SSH restricted to LAN + Tailscale
Google Fonts Removal — Fixed March 23 — Self-hosted system fonts, no external requests
- CSRF Protection — Token-based CSRF on login and state-changing endpoints
- CSP Headers — Content Security Policy + removal of inline event handlers
- Firmware OTA Updates — Heartbeat-driven firmware distribution from fleet server
- QR Code Activation — Scan the QR code on your device to link it to your subscription
- Session IP Binding — Tie sessions to originating IP for defense-in-depth
- DoT Blocking — Block port 853 in doublehop/zhop to prevent DNS bypass via DNS-over-TLS