8cadacb553
gates / gates (push) Successful in 26s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
88 lines
6.4 KiB
Python
88 lines
6.4 KiB
Python
# Part B: a stranger's sign-up through the public route (traefik), each sign-up route of the app in 7 shapes:
|
|
# as written, trailing slash, UPPER, Mixed, one letter percent-encoded, a double slash, a query string.
|
|
# Each answer: BLOCK (the box's "sign-up is closed"), APP-REFUSED (the app's own refusal), or GOT-IN (an account was
|
|
# made / the form was served). A GOT-IN is a hole.
|
|
import json, re, secrets, sys, urllib.parse
|
|
sys.path.insert(0, '.')
|
|
import ro
|
|
from ro import w, host
|
|
from browser import Browser
|
|
|
|
def pw(): return "Xx" + secrets.token_hex(8) + "9!"
|
|
def jn(n): return {"username": n, "email": n + "@x.hu", "password": pw()}
|
|
FORM = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
|
|
# app -> list of (label, method, path, body-maker, headers, got-in test)
|
|
ROUTES = {
|
|
"gitea": [("web form", "GET", "/user/sign_up", None, {}, lambda st, t: st == 200 and 'name="user_name"' in t)],
|
|
"calcom": [("API", "POST", "/api/auth/signup", lambda n: dict(jn(n)), {}, lambda st, t: st in (200, 201)),
|
|
("web page", "GET", "/signup", None, {}, lambda st, t: st == 200 and "Signup is disabled" not in t and "sign-up is closed" not in t and "nem lehet regisztr" not in t and 'name="password"' in t)],
|
|
"gramps-web": [("API", "POST", "/api/users/{n}/register/", lambda n: {"email": n + "@x.hu", "password": pw(), "full_name": "S"}, {}, lambda st, t: st in (200, 201))],
|
|
"homebox": [("API", "POST", "/api/v1/users/register", lambda n: {"name": n, "email": n + "@x.hu", "password": pw()}, {}, lambda st, t: st in (200, 201, 204))],
|
|
"adventurelog": [("web form action", "POST", "/signup", lambda n: urllib.parse.urlencode({"username": n, "email": n + "@x.hu", "password1": "Xx12345678z9!", "password2": "Xx12345678z9!", "first_name": "S", "last_name": "S"}),
|
|
dict(FORM, **{"x-sveltekit-action": "true"}), lambda st, t: '"type":"redirect"' in t and '"location":"/"' in t),
|
|
("allauth API", "POST", "/auth/browser/v1/auth/signup", lambda n: jn(n), {}, lambda st, t: st in (200, 201)),
|
|
("allauth classic", "POST", "/accounts/signup/", lambda n: urllib.parse.urlencode({"username": n, "email": n + "@x.hu", "password1": "Xx12345678z9!", "password2": "Xx12345678z9!"}), FORM, lambda st, t: st in (200, 302) and "sign-up is closed" not in t and "nem lehet" not in t and "CSRF" not in t and "Forbidden" not in t)],
|
|
"papra": [("API", "POST", "/api/auth/sign-up/email", lambda n: {"email": n + "@x.hu", "password": pw(), "name": n}, {}, lambda st, t: st in (200, 201) and "token" in t)],
|
|
"sparkyfitness": [("API", "POST", "/api/auth/sign-up/email", lambda n: {"email": n + "@x.hu", "password": pw(), "name": n}, {}, lambda st, t: st in (200, 201) and "token" in t)],
|
|
"termix": [("API", "POST", "/users/create", lambda n: {"username": n, "password": pw()}, {}, lambda st, t: st in (200, 201) and "User created" in t)],
|
|
"vikunja": [("API", "POST", "/api/v1/register", lambda n: jn(n), {}, lambda st, t: st in (200, 201) and '"id"' in t),
|
|
("web page", "GET", "/register", None, {}, lambda st, t: False)],
|
|
"opengist": [("web form", "POST", "/-/register", "FORM-CSRF", {}, None)],
|
|
"wishlist": [("web form action", "POST", "/signup", lambda n: urllib.parse.urlencode({"name": "S", "username": n, "email": n + "@x.hu", "password": "Xx12345678z9!", "confirmPassword": "Xx12345678z9!"}),
|
|
dict(FORM, **{"x-sveltekit-action": "true"}), lambda st, t: '"type":"success"' in t)],
|
|
}
|
|
|
|
def variants(path):
|
|
base = path.rstrip("/")
|
|
segs = base.split("/")
|
|
last = segs[-1] or segs[-2]
|
|
enc = last[0] + "%" + format(ord(last[1]), "02X") + last[2:] if len(last) > 2 else last
|
|
mixed = "/".join(s[:1].upper() + s[1:] for s in segs)
|
|
lastup = "/".join(segs[:-1] + [segs[-1].upper()])
|
|
out = [("as written", path), ("trailing slash", base + "/"), ("UPPER", base.upper()), ("Mixed", mixed), ("last part UPPER", lastup),
|
|
("percent-encoded", "/".join(segs[:-1] + [enc])), ("double slash", "/" + base), ("query string", base + "?x=1")]
|
|
seen, res = set(), []
|
|
for k, p in out:
|
|
if p not in seen:
|
|
seen.add(p); res.append((k, p))
|
|
return res
|
|
|
|
def classify(st, t, got, m="POST"):
|
|
if "sign-up is closed" in t or "nem lehet regisztr" in t or "Sign-up closed" in t:
|
|
return "BLOCK"
|
|
# A POST answered by the app's HTML shell (its fallback for an unknown address) made no account: the app's own
|
|
# router is case-sensitive, so the tricked address never reached its sign-up code. Measured 2026-09-29: the
|
|
# homebox login of such a name answers "unauthorized".
|
|
if m == "POST" and t.lstrip()[:15].lower().startswith(("<!doctype", "<html")) and got(st, t):
|
|
return "APP-FALLBACK (no account)"
|
|
if got(st, t):
|
|
return "GOT-IN"
|
|
return "APP-REFUSED"
|
|
|
|
w.login()
|
|
for app in sys.argv[1:]:
|
|
for label, m, path, body, hd, got in ROUTES[app]:
|
|
for kind, p in variants(path):
|
|
n = "trk" + secrets.token_hex(3)
|
|
p2 = p.replace("{n}", n).replace("{N}", n.upper())
|
|
s = Browser("stranger")
|
|
h = dict(hd); h["Origin"] = f"https://{host(app)}"
|
|
if body == "FORM-CSRF": # opengist: fetch the form (its CSRF) at the same shape, then post it
|
|
st0, html, _ = s.req(f"https://{host(app)}{p2}")
|
|
mm = re.search(r'name="_csrf" value="([^"]+)"', html)
|
|
if not mm:
|
|
verdict = "BLOCK" if ("nem lehet regisztr" in html or "sign-up is closed" in html) else "APP-REFUSED"
|
|
print(f"{app:14} {label:16} {kind:16} {p2:42} -> {st0} {verdict} (no form served)")
|
|
continue
|
|
b = urllib.parse.urlencode({"username": n, "password": "Xx12345678z9!", "_csrf": mm.group(1)})
|
|
st, t, hops = s.req(f"https://{host(app)}{p2}", "POST", body=b, headers=dict(FORM, Origin=h["Origin"]))
|
|
gotin = st == 200 and hops[-1][2] in ("/", "/-/all") and len(hops) > 1
|
|
verdict = "BLOCK" if ("nem lehet regisztr" in t or "sign-up is closed" in t) else ("GOT-IN" if gotin else "APP-REFUSED")
|
|
print(f"{app:14} {label:16} {kind:16} {p2:42} -> {st} {verdict}")
|
|
continue
|
|
b = body(n) if callable(body) else None
|
|
accept = "text/html" if m == "GET" else "application/json"
|
|
st, t, _ = s.req(f"https://{host(app)}{p2}", m, body=b, accept=accept, headers=h, follow=(m == "GET"))
|
|
print(f"{app:14} {label:16} {kind:16} {p2:42} -> {st} {classify(st, t, got, m)}")
|