Fix 8 code-review findings: security + reliability

1. agent.py: shell allow-check reads cfg, not server payload (RCE fix)
2. webhook.php: move WEBHOOK_SECRET to gitignored config.php; rotate secret
3. agent.py: replace recursive main() with while loop (RecursionError fix)
4. jarvis-deploy.sh: push force-revert to GitHub on syntax fail (loop fix)
5. agent.py: self_update() verifies SHA-256 before exec (integrity fix)
6. agent.php: remove JARVIS_IP from browser-action bypass (auth fix)
7. jarvis-watchdog.sh: escape SQL vars in alert() to prevent injection
8. jarvis-deploy.sh: atomic mv instead of cat+truncate (TOCTOU fix)

Also: distribute jarvis-agent.py.sha256 alongside agent for integrity checks
This commit is contained in:
2026-05-25 14:27:27 +00:00
parent 45fef11785
commit ecbc2e09a5
7 changed files with 99 additions and 27 deletions
+32 -9
View File
@@ -331,9 +331,9 @@ def execute_command(cmd: dict) -> dict:
return {"success": True, "updated": updated}
elif cmd_type == "shell":
# Only allow if explicitly enabled in config
if not cmd_data.get("allowed", False):
return {"success": False, "error": "Shell commands not enabled"}
# Guard reads LOCAL config, not the server-supplied payload
if not cfg.get("allow_shell_commands", False):
return {"success": False, "error": "Shell commands not enabled in agent config"}
cmd_str = cmd_data.get("command", "")
r = subprocess.run(cmd_str, shell=True, capture_output=True, text=True, timeout=30)
return {"success": True, "stdout": r.stdout[:2000], "stderr": r.stderr[:500]}
@@ -359,15 +359,13 @@ def main():
poll_interval = int(cfg.get("poll_interval", 30))
heartbeat_every = int(cfg.get("heartbeat_every", 10))
# Register if no API key yet
# Register if no API key yet — loop (not recurse) to avoid stack overflow
api_key = state.get("api_key", "")
if not api_key:
while not api_key:
api_key = register(cfg, state)
if not api_key:
print("[ERROR] Could not register with JARVIS. Retrying in 60s...", flush=True)
time.sleep(60)
main()
return
headers = {"X-Agent-Key": api_key}
last_metrics = 0
@@ -424,7 +422,9 @@ def main():
# ── Self-update ────────────────────────────────────────────────────────────────
def self_update(cfg: dict) -> bool:
"""Check JARVIS server for a newer version of this script. If different, replace and restart."""
"""Check JARVIS server for a newer version of this script.
Verifies SHA-256 hash from <update_url>.sha256 before replacing."""
import hashlib
jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
default_update_url = f"{jarvis_url}/agent/jarvis-agent.py" if jarvis_url else ""
update_url = cfg.get("update_url", default_update_url)
@@ -432,14 +432,37 @@ def self_update(cfg: dict) -> bool:
return False
script_path = os.path.abspath(__file__)
try:
# Download expected hash first
hash_url = update_url + ".sha256"
req_hash = urllib.request.Request(hash_url)
req_hash.add_header("User-Agent", "JARVIS-Agent/1.0")
if _host_header:
req_hash.add_header("Host", _host_header)
try:
with urllib.request.urlopen(req_hash, timeout=10) as resp:
expected_hash = resp.read().decode().strip().split()[0]
except Exception:
expected_hash = None
# Download new script
req = urllib.request.Request(update_url)
req.add_header("User-Agent", "JARVIS-Agent/1.0")
if _host_header:
req.add_header("Host", _host_header)
with urllib.request.urlopen(req, timeout=30) as resp:
new_content = resp.read()
# Verify hash if available — abort if mismatch
if expected_hash:
actual_hash = hashlib.sha256(new_content).hexdigest()
if actual_hash != expected_hash:
print(f"[JARVIS] Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting", flush=True)
return False
with open(script_path, "rb") as f:
current = f.read()
if new_content != current:
print(f"[JARVIS] Update available — replacing {script_path} and restarting...", flush=True)
print(f"[JARVIS] Update verified — replacing {script_path} and restarting...", flush=True)
with open(script_path, "wb") as f:
f.write(new_content)
os.execv(sys.executable, [sys.executable] + sys.argv)