daacf84e30
gates / gates (push) Successful in 25s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
55 lines
3.1 KiB
Python
55 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""R-747 on 9202: mealie with SECURITY_USER_LOCKOUT_TIME=1 (drill catalog). As a STRANGER (no session, no gate
|
|
cookie): five wrong passwords for the public login, then the household's right password — is it refused, and for how
|
|
long? Polled every 2 minutes with the RIGHT password (a refused try while locked is not counted by mealie)."""
|
|
import subprocess, time, json, sys
|
|
from datetime import datetime, timezone
|
|
import walk as w
|
|
|
|
def now():
|
|
return datetime.now(timezone.utc).strftime("%H:%M:%S")
|
|
|
|
def token(user, pw):
|
|
r = subprocess.run(["curl", "-sk", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "15", "-H", f"Host: recipes.{w.DOMAIN}",
|
|
"--data-urlencode", f"username={user}", "--data-urlencode", f"password={pw}", f"{w.BASE}/api/auth/token"],
|
|
capture_output=True, text=True)
|
|
return r.stdout.strip()
|
|
|
|
def p(*a):
|
|
print(now(), *a, flush=True)
|
|
|
|
w.login()
|
|
for _ in range(10):
|
|
w.sync_rescan()
|
|
if "SECURITY_USER_LOCKOUT_TIME=1" in w.guest("grep -h SECURITY_USER_LOCKOUT /var/lib/docker/volumes/felhom-controller-data/_data/catalog-cache/templates/mealie/docker-compose.yml /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache/templates/mealie/docker-compose.yml 2>/dev/null"):
|
|
break
|
|
time.sleep(20)
|
|
p("the box's catalog copy:", w.guest("grep -rh SECURITY_USER_LOCKOUT /var/lib/docker/volumes/felhom-controller-data/_data/ --include=docker-compose.yml 2>/dev/null | sort -u").strip())
|
|
p("deploy:", w.deploy("mealie", "recipes"))
|
|
for _ in range(120):
|
|
time.sleep(5)
|
|
if "hold OPENED" in w.guest("docker logs --since 15m felhom-controller 2>&1 | grep 'mealie: install hold OPENED'"):
|
|
break
|
|
p(w.guest("docker logs --since 15m felhom-controller 2>&1 | grep -E 'mealie.*(install HOLD|hold OPENED|after_install)' | cut -c1-200").strip())
|
|
p("setting inside the app:", w.guest("docker exec mealie python3 -c 'from mealie.core.config import get_app_settings as g; s=g(); print(s.SECURITY_MAX_LOGIN_ATTEMPTS, s.SECURITY_USER_LOCKOUT_TIME)'").strip())
|
|
gen = (w.GENERATED.get("mealie") or {}).get("ADMIN_PASSWORD", "")
|
|
p("generated password length:", len(gen))
|
|
p("positive control — right password:", token("changeme@example.com", gen))
|
|
for i in range(1, 7):
|
|
p(f"stranger wrong try {i}:", token("changeme@example.com", f"wrong-{i}-{time.time()}"))
|
|
t0 = time.time()
|
|
p("household, RIGHT password, right after:", token("changeme@example.com", gen))
|
|
p("household, RIGHT password by username 'admin':", token("admin", gen))
|
|
code = None
|
|
while time.time() - t0 < 2.5 * 3600:
|
|
time.sleep(120)
|
|
code = token("changeme@example.com", gen)
|
|
p(f"+{(time.time()-t0)/60:.0f} min right password:", code)
|
|
if code == "200":
|
|
break
|
|
p(f"RESULT: locked for {(time.time()-t0)/60:.0f} min (last answer {code})")
|
|
p("right password again:", token("changeme@example.com", gen), "| a wrong one after:", token("changeme@example.com", "wrong-after"))
|
|
p(w.guest("docker logs mealie 2>&1 | grep -iE 'lock' | tail -5 | cut -c1-200"))
|
|
w.remove("mealie")
|
|
p("removed:", w.stack("mealie").get("deployed"))
|