mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
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:
+32
-9
@@ -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)
|
||||
|
||||
@@ -53,7 +53,7 @@ if ($agentAction !== 'register') {
|
||||
if (in_array($agentAction, $browserActions)) {
|
||||
$token = $_SESSION['jarvis_token'] ?? '';
|
||||
$localIP = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
if (empty($token) && !in_array($localIP, ['127.0.0.1', '::1', JARVIS_IP])) {
|
||||
if (empty($token) && !in_array($localIP, ['127.0.0.1', '::1'])) {
|
||||
agent_error(401, 'Unauthorized');
|
||||
}
|
||||
$agent = null;
|
||||
|
||||
+15
-5
@@ -13,9 +13,12 @@ log() { echo "[$(TS)] $1" >> "$LOG"; }
|
||||
[ ! -f "$QUEUE" ] && exit 0
|
||||
[ ! -s "$QUEUE" ] && exit 0
|
||||
|
||||
# Snapshot and clear queue atomically
|
||||
SNAPSHOT=$(cat "$QUEUE")
|
||||
> "$QUEUE"
|
||||
# Atomically take ownership of the queue via rename — prevents TOCTOU loss of
|
||||
# entries written between a cat and truncate
|
||||
PROCESSING="${QUEUE}.processing"
|
||||
mv "$QUEUE" "$PROCESSING" 2>/dev/null || exit 0
|
||||
SNAPSHOT=$(cat "$PROCESSING")
|
||||
rm -f "$PROCESSING"
|
||||
|
||||
while IFS= read -r path; do
|
||||
[ -z "$path" ] && continue
|
||||
@@ -51,13 +54,20 @@ while IFS= read -r path; do
|
||||
done <<< "$CHANGED"
|
||||
|
||||
if [ "$SYNTAX_OK" = false ]; then
|
||||
log "SYNTAX ERROR in $BAD_FILE — reverting to $BEFORE"
|
||||
log "SYNTAX ERROR in $BAD_FILE — reverting locally and pushing revert to GitHub"
|
||||
git reset --hard "$BEFORE" >> "$LOG" 2>&1
|
||||
# Push the revert so GitHub matches the live server — prevents infinite re-deploy loop
|
||||
git push --force origin HEAD:main >> "$LOG" 2>&1
|
||||
PUSH_EXIT=$?
|
||||
if [ $PUSH_EXIT -ne 0 ]; then
|
||||
log "WARNING: Force-push of revert failed (exit $PUSH_EXIT) — bad commit still on GitHub"
|
||||
fi
|
||||
# Insert alert into JARVIS DB
|
||||
BAD_ESCAPED=$(printf '%s' "$BAD_FILE" | sed "s/'/\\\\\\'/g")
|
||||
mysql -u jarvis_user -pJ4rv1s_Pr0t0c0l_2026! jarvis_db -se \
|
||||
"INSERT INTO alerts (alert_type,title,message,severity)
|
||||
VALUES ('deploy_fail','Deploy reverted: syntax error',
|
||||
'PHP syntax error in $BAD_FILE. Commit $AFTER was reverted automatically.','critical');" 2>/dev/null
|
||||
'PHP syntax error in $BAD_ESCAPED. Commit $AFTER was reverted and force-pushed to GitHub.','critical');" 2>/dev/null
|
||||
log "Reverted. Bad commit: $AFTER"
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -10,15 +10,23 @@ TS() { date '+%Y-%m-%d %H:%M:%S'; }
|
||||
|
||||
log() { echo "[$(TS)] $1" >> "$LOG"; }
|
||||
|
||||
# Escape single quotes for MySQL string interpolation in bash
|
||||
sql_esc() { printf '%s' "$1" | sed "s/'/\\\\''/g"; }
|
||||
|
||||
alert() {
|
||||
local type="$1" title="$2" msg="$3" sev="${4:-warning}"
|
||||
local e_type e_title e_msg e_sev
|
||||
e_type=$(sql_esc "$type"); e_title=$(sql_esc "$title")
|
||||
e_msg=$(sql_esc "$msg"); e_sev=$(sql_esc "$sev")
|
||||
$MYSQL "INSERT IGNORE INTO alerts (alert_type,title,message,severity,source_key,auto_resolve)
|
||||
VALUES ('$type','$title','$msg','$sev','watchdog:$type',1);" 2>/dev/null
|
||||
VALUES ('$e_type','$e_title','$e_msg','$e_sev','watchdog:$e_type',1);" 2>/dev/null
|
||||
}
|
||||
|
||||
resolve() {
|
||||
local e_key
|
||||
e_key=$(sql_esc "$1")
|
||||
$MYSQL "UPDATE alerts SET resolved=1,resolved_at=NOW()
|
||||
WHERE source_key='watchdog:$1' AND resolved=0;" 2>/dev/null
|
||||
WHERE source_key='watchdog:$e_key' AND resolved=0;" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Service health ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
6c93ea50f3a91472444a10838b89d4222b8378cd153efee4ed9f75d7d5fb25b2
|
||||
@@ -3,9 +3,16 @@
|
||||
* GitHub Auto-Deploy Webhook
|
||||
* Verifies GitHub HMAC signature, then queues the repo for git pull.
|
||||
* A root cron job (/usr/local/bin/jarvis-deploy.sh) processes the queue every minute.
|
||||
*
|
||||
* WEBHOOK_SECRET is loaded from api/config.php (gitignored) — never hardcoded here.
|
||||
*/
|
||||
|
||||
define('WEBHOOK_SECRET', '8a8c50c83d37527bdef876f1736b654235724a1a475cb8e5');
|
||||
require_once __DIR__ . '/../../api/config.php';
|
||||
if (!defined('WEBHOOK_SECRET')) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Webhook not configured']);
|
||||
exit;
|
||||
}
|
||||
define('DEPLOY_QUEUE', '/tmp/jarvis-deploy-queue.txt');
|
||||
define('DEPLOY_LOG', '/home/jarvis.orbishosting.com/logs/deploy.log');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user