← All manuals Operator Guide for this screen IM2

Sign in

Sign in
Technical Spec Ver 2 Updated 2026-09-06 Matches the live screen

Sign in — Technical Spec

app/im2.py, template app/templates/login.html. Interim auth until Slack OAuth credentials exist (Jeff's decision, 2026-08-23).

Routes

MethodPathAuthNotes
GET/loginnoneRenders login.html with user=None (so base.html renders no header/nav). ?error=1 shows the one generic failure banner.
POST/loginnoneForm email, pin.
GET/logoutnonedelete_cookie("im2"), 303 → /login.
GET/healthznoneUnauthenticated: returns {"ok":true,"active_items":N}.

Everything else depends on current_user.

Credential check

Lookup: select username, pin_hash from app_users where lower(email) = lower(%s) and active.

Session

Known limits / security weaknesses (flagged plainly)

  1. No lockout and no rate limiting anywhere. There is no failed-attempt counter, no delay, no IP throttle, no captcha. Combined with a 4-digit numeric PIN and a known email pattern (first.last@solalt.com), the credential space is ~10,000 guesses; pbkdf2 at 120k iterations is the only brake, and it costs the server, not the attacker. This is the single biggest security gap in the app. Cheapest mitigations: a per-email failed-attempt table with a lockout, and a 6-digit minimum PIN.
  2. No failed-login logging. Nothing is written on a bad attempt, so a brute-force attempt would leave no trace in audit_log at all.
  3. secure=True is not set on the cookie, so it will be sent over plain HTTP if the app is ever reachable that way (any reverse-proxy misconfiguration downgrades to a clear-text session).
  4. URLSafeSerializer is untimestamped — cookie lifetime is enforced only by the browser's max_age. A copied cookie value is valid forever while the user stays active and the secret is unchanged. URLSafeTimedSerializer + a max_age on loads() would fix this.
  5. Setting or regenerating a PIN does not invalidate existing sessions; a compromised session survives the password reset that was meant to close it.
  6. No CSRF protection on any POST//api/* route. samesite="lax" blocks cross-site form posts in current browsers, which is the only reason this is not already exploitable.
  7. IM2_SECRET defaulting to a random per-process value means a multi-worker deployment without the env var set produces random 401 → /login bounces (each worker rejects the others' cookies) rather than a clean failure.
  8. Only an admin can set a PIN; there is no self-service reset, so a lost PIN is a Slack message to Alex or Dave. Deliberate for now — worth noting as an operational load, not a defect.
  9. The whole /docs tree (docs.py::install) has no current_user dependency — its docstring says "readable by anyone signed in", but in fact any Operator Help or Technical Spec page, including this one, is readable without a session. The tech pages name tables, columns and route paths.
  10. /healthz is unauthenticated and discloses the active item count. Harmless, but it is a public endpoint on this host.

CSRF protection (added 2026-09-06)

same_origin_only, an HTTP middleware in im2.py mounted immediately after the app is created, rejects any non-GET/HEAD/OPTIONS request whose Origin (or, for plain form posts, Referer) host does not equal the request Host, with a 403 and a plain operator message. A request carrying no session cookie and no origin is allowed through, so health probes and the route verifier still work. Blocked attempts are logged with method, path, origin and referer.

Chosen over per-form CSRF tokens deliberately: every write in the app is a POST from one of our own pages, so an origin check gives the same protection without a hidden token in ~40 templates that would drift out of step with them. The session cookie is also samesite="strict" now, so most browsers will not send it on a cross-site request at all. Verified live: a POST with Origin: https://evil.com and a POST carrying a cookie but no origin are both refused; the real login POST still works.