CAP-Warn Security & Bug Review Report

Version 1.0 · Date 2026-07-02 · CAP-Warn full codebase static review · 39 total findings

Executive Summary

CAP-Warn is a PHP-based weather alert system for AllStar/repeater nodes. It fetches
NWS CAP alerts, optional NHC cyclone data, Raspberry Pi temperature monitoring, TTS,
and Pushover notifications. The most serious issues are shell command injection
surfaces, insecure storage/logging of API credentials, assignment-instead-of-comparison
bugs that break Pi alert behavior, and broken severe-weather audio logic due to case
mismatch.

Summary Counts

6
Critical Security
5
High Security
4
Medium Security
3
Low Security
4
Critical Bugs
8
High Bugs
7
Medium Bugs
5
Low Bugs

Quick Reference Table

IDSeverityCategoryFileLinesFinding
BUG-001 Critical Bug usr/share/cap-warn/cap_temp.php 148, 192 Assignment (=) instead of comparison (==) — breaks Pi pushover modes
BUG-002 Critical Bug usr/share/cap-warn/cap_functions.php 943, 970 Call to undefined function add_word()
BUG-003 Critical Bug usr/share/cap-warn/cap_warn.php 384, 389-415 Severe weather "seek shelter" audio never plays (case bug)
BUG-004 Critical Bug usr/share/cap-warn/cap_temp.php 177 Undefined variable $high — emergency temperature tier broken
SEC-001 Critical Security usr/share/cap-warn/cap_functions.php 566, 574-575, 52, 977, 704 Command injection via unescaped shell arguments
SEC-002 Critical Security usr/share/cap-warn/setup.sh 575-602 API keys and tokens stored without restrictive permissions
SEC-003 Critical Security usr/share/cap-warn/setup.sh 223, 560-561 Secrets written to install logs in plaintext
SEC-004 Critical Security usr/bin/cap-warn/cap_warn.sh 12-18 World-writable runtime state files
SEC-005 Critical Security usr/bin/cap-warn/net-flag.sh 6, 10 Hub mute flag controllable by any local user
SEC-006 Critical Security usr/share/cap-warn/cap_functions.php 942-943 Overly permissive TTS temp directory
BUG-005 High Bug usr/share/cap-warn/cap_functions.php 819-977 Missing global $node in check_sound()
BUG-006 High Bug usr/share/cap-warn/cap_cyclones.php 49-57 Cyclone module: quiet hours broken when run standalone
BUG-007 High Bug usr/share/cap-warn/setup.sh 523 Setup script typo: $PIALARMS vs $piAlarms
BUG-008 High Bug etc/cap-warn/advanced_config.php 124-125 Double PHP closing tag in advanced config
BUG-009 High Bug usr/share/cap-warn/cap_temp.php 59 Wrong vcgencmd invocation
BUG-010 High Bug usr/share/cap-warn/cap_cyclones.php 190-191 Cyclone playback picks wrong file when VoiceRSS fails
BUG-011 High Bug usr/share/cap-warn/cap_functions.php 424-426 merge_alerts() crashes on invalid JSON
BUG-012 High Bug usr/share/cap-warn/cap_functions.php 163 isNewCycloneAdvisory() marks storm announced before playback
SEC-007 High Security usr/share/cap-warn/voicerss_tts.php 36 VoiceRSS API defaults to HTTP
SEC-008 High Security usr/share/cap-warn/cap_functions.php 326-337, 377-388, 438-449, 493-500 No TLS hardening on external HTTP clients
SEC-009 High Security usr/share/cap-warn/cap_functions.php 574-575 Privilege escalation via sudo Asterisk
SEC-010 High Security usr/share/cap-warn/cap_functions.php 71-78, 919 Unbounded log growth / sensitive data in archives
SEC-011 High Security usr/share/cap-warn/cap_pushover.php 36-67 Pushover messages lack input sanitization
BUG-013 Medium Bug usr/share/cap-warn/cap_warn.php 144-151 Misleading hold-flag message; playback not fully skipped
BUG-014 Medium Bug usr/share/cap-warn/cap_temp.php 128-141 Inverted/confusing low-voltage counter logic
BUG-015 Medium Bug usr/share/cap-warn/cap_temp.php 53, 157, 176, 183 Empty Pushover notifications for temperature
BUG-016 Medium Bug usr/share/cap-warn/cap_functions.php 463 fetchCapAlerts() deletes cache on network failure
BUG-017 Medium Bug usr/share/cap-warn/cap_functions.php 366 fetchZoneAlerts() may reference undefined $elapsed
BUG-018 Medium Bug usr/share/cap-warn/cap_functions.php 614 serial() deletes instance.id on every run
BUG-019 Medium Bug usr/share/cap-warn/setup.sh 635 Setup references undefined $BACKUP
BUG-020 Medium Bug etc/cap-warn/config.php 9-33 Placeholder config forces die until setup runs
BUG-021 Medium Bug usr/share/cap-warn/cap_warn.php 169-179 Clash-file mutual exclusion is weak
BUG-022 Medium Bug usr/share/cap-warn/cap_warn.php 96-97 GPS coordinate validation strips but does not validate range
BUG-023 Medium Bug etc/cap-warn/advanced_config.php 40 $reportAll config option unused
LOW-001 Low Bug usr/share/cap-warn/cap_warn.php 252 Deprecated <> operator
LOW-002 Low Security usr/share/cap-warn/cap_functions.php 589 cvlc path quoting fragile
LOW-003 Low Bug usr/share/cap-warn/cap_cyclones.php 98 Misleading cyclone log message
LOW-004 Low Bug usr/share/cap-warn/cap_temp.php 110 Throttle sound lookup likely fails
LOW-005 Low Bug usr/share/cap-warn/cap_functions.php 731 GPS HDOP rating typo

Critical Security Issues

[SEC-001] Command injection via unescaped shell arguments

usr/share/cap-warn/cap_functions.php : 566, 574-575, 52, 977, 704

Multiple exec()/shell_exec() calls interpolate variables without escapeshellarg(). $action is built from config, API text, and filesystem paths and passed to sox. $node is passed unescaped to asl-tts and sudo asterisk. $device from glob() is unescaped in cat command.

Related: usr/share/cap-warn/cap_warn.php:498

Impact: Local or config-level attackers may execute arbitrary commands as the script user (often root via cron/sudo).

[SEC-002] API keys and tokens stored without restrictive permissions

usr/share/cap-warn/setup.sh : 575-602

setup.sh writes /etc/cap-warn/config.php with VoiceRSS key, Pushover user key, and API token. No chmod 600 or ownership hardening applied.

Related: usr/share/cap-warn/setup_text.sh:251-275

Impact: Any local user may read Pushover/VoiceRSS credentials from disk (default umask often yields world-readable config).

[SEC-003] Secrets written to install logs in plaintext

usr/share/cap-warn/setup.sh : 223, 560-561

Full VoiceRSS API key logged to /var/log/cap-warn/install.log. Summary screen includes Pushover key and token.

Impact: Credential exposure to anyone who can read install logs.

[SEC-004] World-writable runtime state files

usr/bin/cap-warn/cap_warn.sh : 12-18

Sets /tmp/alert*, /tmp/warn*, /tmp/cyclone* files to chmod 666.

Related: usr/bin/cap_warn:12-18

Impact: Any local user can modify alert cache, hold flags, cyclone state, or playback flags — suppress alerts, force replays, or corrupt behavior.

[SEC-005] Hub mute flag controllable by any local user

usr/bin/cap-warn/net-flag.sh : 6, 10

Creates /tmp/linked_true.txt with no privilege check. cap_warn.php aborts all playback when this file exists.

Related: etc/cap-warn/advanced_config.php:49, usr/share/cap-warn/cap_warn.php:157-163

Impact: Unprivileged users can mute the repeater by touching a world-writable /tmp path, or unmute by deleting it.

[SEC-006] Overly permissive TTS temp directory

usr/share/cap-warn/cap_functions.php : 942-943

mkdir($ttsDir, 0777, true) for /tmp/tts — world-writable directory.

Impact: Local users can plant or replace TTS audio before Sox conversion and playback.

High Security Issues

[SEC-007] VoiceRSS API defaults to HTTP

usr/share/cap-warn/voicerss_tts.php : 36

URL is http://api.voicerss.org/ unless ssl is set in settings. TTS text and API key sent in cleartext.

Impact: Network attackers can capture API key and alert content.

[SEC-008] No TLS hardening on external HTTP clients

usr/share/cap-warn/cap_functions.php : 326-337, 377-388, 438-449, 493-500

cURL calls to NWS/NHC with no explicit TLS options (min version, pinning). Pushover uses HTTPS but lacks response validation.

Impact: Relies on system defaults; no defense-in-depth for MITM.

[SEC-009] Privilege escalation via sudo Asterisk

usr/share/cap-warn/cap_functions.php : 574-575

Runs sudo asterisk -rx with interpolated $node. Compromised config.php $node can inject Asterisk CLI commands.

Impact: If passwordless sudo is configured, affects telephony stack integrity.

[SEC-010] Unbounded log growth / sensitive data in archives

usr/share/cap-warn/cap_functions.php : 71-78, 919

archive_json() appends full alert JSON to archive.log with no size cap. sound_cache.csv grows via FILE_APPEND without bounds.

Impact: Disk exhaustion; long-term retention of location-specific alert data.

[SEC-011] Pushover messages lack input sanitization

usr/share/cap-warn/cap_pushover.php : 36-67

Weather/cyclone/Pi status text passed directly to Pushover without length or content filtering.

Impact: Low direct risk; NWS alert text forwarded as-is to third-party service.

Critical Bugs

[BUG-001] Assignment (=) instead of comparison (==) — breaks Pi pushover modes

usr/share/cap-warn/cap_temp.php : 148, 192

if($pushNotify = "pi_pushover") assigns and is always true. Clears $action and suppresses voice regardless of configured mode.

Impact: pi_pushover, pi_both, and all modes behave incorrectly; temperature voice alerts likely never play when those lines execute.

[BUG-002] Call to undefined function add_word()

usr/share/cap-warn/cap_functions.php : 943, 970

Calls add_word($in) on TTS failure paths, but add_word() is fully commented out (lines 1060-1085).

Impact: PHP fatal error when TTS directory creation or sound file write fails — can stop entire cron job.

[BUG-003] Severe weather "seek shelter" audio never plays (case bug)

usr/share/cap-warn/cap_warn.php : 384, 389-415

Event names lowercased with strtolower(), then compared to mixed-case strings like "Tornado Warning". Comparisons never match.

Impact: Extra shelter/tornado/thunderstorm/hurricane/blizzard/burn-ban audio skipped on initial alert. Tail-file loop (453-487) does not lowercase — inconsistent behavior.

[BUG-004] Undefined variable $high — emergency temperature tier broken

usr/share/cap-warn/cap_temp.php : 177

if ($temp >= $high) references $high which is never defined. Only commented in advanced_config.php line 38.

Impact: PHP notices; emergency/warning/attention sounds for extreme heat never trigger.

High Bugs

[BUG-005] Missing global $node in check_sound()

usr/share/cap-warn/cap_functions.php : 819-977

check_sound() uses $node in asl-tts command (line 977) without declaring global $node.

Impact: Offline TTS fallback for missing sound files fails or uses empty node ID.

[BUG-006] Cyclone module: quiet hours broken when run standalone

usr/share/cap-warn/cap_cyclones.php : 49-57

Uses $hour and $day for sleep mute, but neither is set in cap_cyclones.php.

Impact: PHP warnings; quiet hours do not apply to cyclone announcements when module is invoked directly.

[BUG-007] Setup script typo: $PIALARMS vs $piAlarms

usr/share/cap-warn/setup.sh : 523

if [ "$PIALARMS" != "true" ] — $PIALARMS is never set; should be $piAlarms.

Impact: Pushover notification mode menu logic is wrong when Pi alarms are configured.

[BUG-008] Double PHP closing tag in advanced config

etc/cap-warn/advanced_config.php : 124-125

Two ?> closers. Trailing ?> can emit whitespace and cause "headers already sent" in web context.

Impact: Potential output corruption if file used via web server.

[BUG-009] Wrong vcgencmd invocation

usr/share/cap-warn/cap_temp.php : 59

exec("sudo /usr/bin/vcgencmd measure_temp 2", ...) — literal 2 passed as argument, not stderr redirection.

Impact: vcgencmd may fail; code falls back to sysfs.

[BUG-010] Cyclone playback picks wrong file when VoiceRSS fails

usr/share/cap-warn/cap_cyclones.php : 190-191

if ($tts) uses cycloneWAV else cycloneUL — ignores build_tts_audio() return value which may be .ul via asl-tts fallback.

Impact: Cyclone alert audio may not play when VoiceRSS is down but offline TTS succeeds.

[BUG-011] merge_alerts() crashes on invalid JSON

usr/share/cap-warn/cap_functions.php : 424-426

If cached file exists but json_decode() returns null, $pointData["features"] causes fatal error.

Impact: Corrupt or partial cache file can crash the alert pipeline.

[BUG-012] isNewCycloneAdvisory() marks storm announced before playback

usr/share/cap-warn/cap_functions.php : 163

Appends storm key to cyclone_key.log before audio is built or played.

Impact: TTS or playback failure permanently suppresses re-announcement of that advisory.

Medium Bugs

[BUG-013] Misleading hold-flag message; playback not fully skipped

usr/share/cap-warn/cap_warn.php : 144-151

Logs "Skipping playback — still within hold window" but does not return or set skip flag. New alerts still play.

Impact: Misleading logs; hold window only partially enforced.

[BUG-014] Inverted/confusing low-voltage counter logic

usr/share/cap-warn/cap_temp.php : 128-141

$LoV_limit comment says "max alarms per day" but logic suppresses while count <= limit. Opposite pattern from $uv_limit. $LoV__counter defined but never used.

Impact: Confusing behavior; requires 12+ events before alerting when limit=10.

[BUG-015] Empty Pushover notifications for temperature

usr/share/cap-warn/cap_temp.php : 53, 157, 176, 183

$out initialized as "" and never set to temperature message before send_pushover_notification($out).

Impact: Pushover receives blank or useless messages for temp events.

[BUG-016] fetchCapAlerts() deletes cache on network failure

usr/share/cap-warn/cap_functions.php : 463

unlink($cacheFile) on HTTP failure removes last good alert data.

Impact: Transient NWS outage can clear alerts and trigger false "all clear" behavior.

[BUG-017] fetchZoneAlerts() may reference undefined $elapsed

usr/share/cap-warn/cap_functions.php : 366

Prints $elapsed after loop; if both zone URLs fail, variable may be unset.

Impact: PHP notice/warning in logs.

[BUG-018] serial() deletes instance.id on every run

usr/share/cap-warn/cap_functions.php : 614

Unlinks /etc/cap-warn/instance.id every time serial() runs.

Impact: Unexpected side effect; any future use of instance ID is broken.

[BUG-019] Setup references undefined $BACKUP

usr/share/cap-warn/setup.sh : 635

Completion dialog shows $BACKUP but variable is never assigned.

Impact: Empty/misleading installer completion message.

[BUG-020] Placeholder config forces die until setup runs

etc/cap-warn/config.php : 9-33

Ships as die stub until setup runs. Mis-deployed package breaks all cron runs.

Impact: No graceful degradation without manual setup.

[BUG-021] Clash-file mutual exclusion is weak

usr/share/cap-warn/cap_warn.php : 169-179

Waits 90s if clash file exists, then writes own flag. No file locking; race between concurrent cron instances.

Impact: Duplicate playback or indefinite 90s delays if process dies without cleanup.

[BUG-022] GPS coordinate validation strips but does not validate range

usr/share/cap-warn/cap_warn.php : 96-97

Strips non-numeric chars from lat/lon but does not validate -90..90 / -180..180.

Impact: Invalid coords still hit NWS API.

[BUG-023] $reportAll config option unused

etc/cap-warn/advanced_config.php : 40

$reportAll=false documented for temp debugging but never referenced in cap_temp.php.

Impact: Dead configuration option.

Low Severity Issues

[LOW-001] Deprecated <> operator

usr/share/cap-warn/cap_warn.php : 252

Uses <> (deprecated alias for !=).

Impact: Style/compat only.

[LOW-002] cvlc path quoting fragile

usr/share/cap-warn/cap_functions.php : 589

cvlc invoked with single-quoted $file — breaks if path contains apostrophe.

Impact: Edge case playback failure or injection if path is attacker-controlled.

[LOW-003] Misleading cyclone log message

usr/share/cap-warn/cap_cyclones.php : 98

Prints "Hurricanes Active" in else branch when hurOnly is false (all tropical systems).

Impact: Log confusion only.

[LOW-004] Throttle sound lookup likely fails

usr/share/cap-warn/cap_temp.php : 110

Throttle messages joined with "_ " produce names like under-voltage-detected_ arm-frequency-capped.

Impact: Likely no matching sound file for combined throttle state.

[LOW-005] GPS HDOP rating typo

usr/share/cap-warn/cap_functions.php : 731

Typo "Greatt" in GPS HDOP rating string.

Impact: Cosmetic/log noise only.

Security Posture by Component

Untrusted Inputs: - NWS/NHC APIs - GPS/NMEA devices - /tmp flag files - config.php Core: - cap_warn.php - cap_functions.php - cap_temp.php - cap_cyclones.php Privileged Outputs: - sudo asterisk - sox shell - Pushover API - VoiceRSS API - /var/log/cap-warn Highest-Risk Paths: - config → shell ($node, $action) - /tmp flags → alert suppression - secrets on disk → credential theft

Recommended Fix Priority

  1. Fix = vs == in cap_temp.php (lines 148, 192)
  2. Restore or remove add_word() calls
  3. Fix tornado/warning string comparisons (use lowercase literals or case-insensitive compare)
  4. Define $high or remove the branch; fix empty Pushover $out
  5. Escape all shell arguments (escapeshellarg for $node, $device, and each file in $action)
  6. chmod 600 on config.php; stop logging API keys
  7. Restrict /tmp state files (root-only, not 666)
  8. Enable HTTPS for VoiceRSS; tighten /tmp/tts permissions
  9. Harden merge_alerts() against null JSON
  10. Fix cyclone file-selection logic and advisory dedup timing