Files
UmbraTrans/web/reset-password.html
umbra audio 3419485aa7 Fix a syntax error I introduced, which put a password in a URL
The commit immediately before this one - the password-toggle fix - deleted a closing
brace and did not replace it. app.js did not parse. The browser reported it in a console
nobody was watching, and every script-driven behaviour on the sign-in page stopped.

The consequence was not a cosmetic one. The login form's submit handler, which is the
only thing calling preventDefault(), never ran, so the browser performed a native form
submission. Neither form declared a method, and a form with no method attribute submits
as GET. Both password fields went into the query string:

    GET /?email=umbra%40bdmurphy.me&password=...

The server logged it in plain text, three times, before it was noticed. It would also
have sat in browser history and in any proxy access log between here and the public
internet.

Two fixes, the second of which is the one that matters:

- The toggle fix is reapplied correctly to the last known-good version of app.js, with
  nothing else in the file touched - the diff is four lines against thirteen and two
  against one.
- **Every form on every page now declares method=post.** The script still intercepts
  and submits via fetch; this decides what happens when the script does not run, which is
  precisely the case that failed. Five more forms carried the same latent defect,
  including password reset and email verification.

test_form_safety.py checks both, and was confirmed to fail when app.js is broken the
same way. It uses esprima rather than a hand-written brace counter, which gave two false
readings during this work - one of them 'balanced' for a file that did not parse at all.
A check that can be wrong about a syntax error is worse than no check.

The account password should be treated as exposed and changed. So should any password
entered into that form while the script was broken.
2026-10-03 20:06:05 -07:00

81 lines
4.0 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Choose a new UmbraTrans password">
<meta name="referrer" content="same-origin">
<title>Choose a new password · UmbraTrans</title>
<link rel="icon" type="image/png" href="assets/umbratrans-emblem.png">
<link rel="stylesheet" href="assets/styles.css">
</head>
<body class="auth-page">
<main class="auth-shell">
<section class="auth-brand" aria-labelledby="brand-title">
<a class="brand-lockup" href="index.html" aria-label="UmbraTrans home">
<img class="brand-image" src="assets/umbratrans-emblem.png" alt="" aria-hidden="true">
<span>UmbraTrans</span>
</a>
<div class="brand-copy">
<p class="eyebrow">Account recovery</p>
<h1 id="brand-title">Choose a new password.</h1>
<p class="brand-lede">This link works once and expires an hour after it was sent. Setting a new password signs out every other device on the account.</p>
</div>
</section>
<section class="auth-card" aria-labelledby="reset-title">
<div id="reset-unavailable" hidden>
<div class="auth-card-heading">
<span class="status-pill status-pill--warning">Link not usable</span>
<h2 id="reset-title">This link is no longer valid</h2>
<p id="reset-unavailable-text">This link may already have been used, or it may have expired. Ask for a new one and try again.</p>
</div>
<a class="admin-link" href="index.html"><span>Back to sign in</span><span aria-hidden="true">↗</span></a>
</div>
<div id="reset-unreachable" hidden>
<div class="auth-card-heading">
<span class="status-pill status-pill--info">Could not check</span>
<h2>We could not check this link</h2>
<p id="reset-unreachable-text">The service could not be reached, so we do not know whether this link still works. That is not the same as it having expired. Check your connection and reload.</p>
</div>
<a class="admin-link" href="index.html"><span>Back to sign in</span><span aria-hidden="true">↗</span></a>
</div>
<div id="reset-form-view" hidden>
<div class="auth-card-heading">
<span class="status-pill status-pill--info">Almost done</span>
<h2 id="reset-title">Choose a new password</h2>
<p id="reset-intro">Pick something you have not used here before. Every other signed-in device will be signed out when you do.</p>
</div>
<!-- method="post": a form with no method defaults to GET, and a native
fallback submission would put password reset into the query string and into
the server log. No action: the script submits via fetch; this only
decides what happens when the script does not run. -->
<form id="reset-form" class="stack-form" method="post" novalidate>
<label for="new-password">New password</label>
<div class="password-field">
<input id="new-password" name="new_password" type="password" autocomplete="new-password" required>
<button id="toggle-new-password" class="password-toggle" type="button" aria-pressed="false" aria-controls="new-password">Show</button>
</div>
<label for="confirm-password">Confirm new password</label>
<input id="confirm-password" name="confirm_password" type="password" autocomplete="new-password" required>
<ul class="policy-list" id="reset-policy">
<li id="policy-length">At least 14 characters.</li>
<li id="policy-match">The two entries must match.</li>
</ul>
<div id="reset-message" class="form-message" role="status" aria-live="polite"></div>
<button id="reset-submit" class="button button--primary button--wide" type="submit">Set new password <span aria-hidden="true">→</span></button>
</form>
</div>
</section>
</main>
<script src="assets/api.js" defer></script>
<script src="assets/reset.js" defer></script>
</body>
</html>