Files
picpeak/backend/src/utils/passwordInput.js
T
Paul NothaftandPaul Nothaft 323dcae917 fix(gallery): block password form in Instagram in-app browser and unmask login errors (#863)
* fix(gallery): block password form in Instagram in-app browser (#654)

Field reports show gallery password login still failing inside
Instagram's IAB after the #656 input-attribute/trim defenses. Three
changes:

- Replace the advisory amber banner with a red blocking state: the
  password form is hidden in the Instagram IAB and replaced with
  platform-specific "open in external browser" instructions plus a
  copy-link button (clipboard API with execCommand fallback). A
  "try anyway" link restores the form as an escape hatch.
- Stop masking non-password failures as "incorrect password": a request
  that never got a response (offline, webview killed it) now reports a
  connection error, and a reCAPTCHA 400 reports a verification failure —
  both previously fell through to the wrong-password message and sent
  guests chasing the wrong cause.
- Strip invisible Unicode (zero-width chars, word joiner, BOM, soft
  hyphen) from the submitted password in addition to trimming — these
  ride along when the password is copy-pasted out of a chat app and fail
  byte-exact bcrypt compare server-side.

* fix(gallery): retry login with typed password + honor execCommand result (#654)

Codex review round 1:
- Stored passwords can legitimately contain the invisible code points the
  sanitizer strips (e.g. ZWJ emoji sequences) — creation paths don't
  normalize. On a 401 where the sanitized form differs from the typed
  (trimmed) input, retry once with the typed value. Skipped when a
  reCAPTCHA token is in play (single-use).
- document.execCommand('copy') signals failure via its return value, not
  by throwing — only show "Link copied" when it returns true.

* fix(gallery): move invisible-char password fallback server-side (#654)

Codex review round 2: the client-side retry either burned the single-use
reCAPTCHA token (making exotic-but-valid passwords impossible to enter
with reCAPTCHA on) or burned failed-attempt lockout quota on every
rescued login. Doing the fallback as a second bcrypt compare inside the
same gallery/verify request eliminates both: exact bytes are compared
first (stored passwords containing e.g. ZWJ emoji keep working), the
sanitized form only on mismatch, and trackFailedAttempt only fires when
both fail. Frontend goes back to plain trim-on-submit; the client-side
sanitizer util and retry are removed. 7 integration tests pin the
contract.

---------

Co-authored-by: Paul Nothaft <[email protected]>
2026-07-23 21:45:07 +02:00

22 lines
917 B
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Strip invisible Unicode from a guest-submitted gallery password (#654).
*
* Gallery passwords are usually relayed to guests through chat apps —
* Instagram DMs especially — and copy-pasting from those surfaces drags
* invisible characters along with the visible ones: zero-width
* space/joiners (U+200BU+200D), word joiner (U+2060), BOM (U+FEFF), soft
* hyphen (U+00AD). Those fail the byte-exact bcrypt compare with a plain
* "incorrect password" verdict and no visible cause.
*
* Used by the gallery verify route as a same-request compare fallback: the
* exact submitted bytes are always tried first, so stored passwords that
* legitimately contain these characters keep working.
*/
const INVISIBLE_CHARS = /[\u200B-\u200D\u2060\uFEFF\u00AD]/g;
function sanitizePasswordInput(raw) {
return String(raw).replace(INVISIBLE_CHARS, '').trim();
}
module.exports = { sanitizePasswordInput };