diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index a315fa0..4393238 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -146,6 +146,11 @@ jobs: # credentials offered on an attacker's neighbouring subdomain. run: node tests/js/test_psl.js + - name: Field heuristic tests + # Guards login-field detection. A wrong answer here means autofill + # silently does nothing on real login pages. + run: node tests/js/test_field_heuristics.js + # ── Test suite ─────────────────────────────────────────────────────────────── # Runs against in-memory SQLite (see app/config.py TestingConfig) so no MySQL # service is needed on the host-mode runner. That means these tests cover diff --git a/CLAUDE.md b/CLAUDE.md index 6090a3b..f7ae03b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -121,7 +121,9 @@ passkeeper/ │ ├── test_registration_privacy.py # register does not disclose account existence │ ├── test_emergency_visibility.py # grantor sees requests + retrievals │ ├── test_deploy_config.py # nginx/gunicorn/systemd/extension packaging guards -│ └── js/test_psl.js # PSL same-site matching (node, run in CI) +│ └── js/ +│ ├── test_psl.js # PSL same-site matching (node, run in CI) +│ └── test_field_heuristics.js # login-field detection predicates ├── gunicorn.conf.py # worker class, timeouts, preload_app=False ├── pytest.ini ├── requirements-dev.txt @@ -618,7 +620,32 @@ In both `content.js` and `popup.js`. Prevents silent match failures for bare dom 1. **YES:** `autocomplete="username|email|tel"` 2. **NO:** non-credential autocomplete (`name`, `organization`, `search`, etc.) 3. **YES:** `name/id/placeholder/aria-label` matches `user|email|mail|login|phone|tel|mobile|account` -4. **Otherwise:** not decorated +4. **Then:** `_hasPasswordSibling()` must also pass +5. **Otherwise:** not decorated + +**`autocomplete="off"` is NOT a negative signal** and must never be added back to +`NON_CRED_AC`. Routers, banks and admin panels set it on login fields precisely +to discourage password managers. It previously caused step 2 to reject fields as +obvious as `` +before step 3 ever ran (ASUS RT-AX88U admin login). Letting it fall through is +safe — the field still needs a credential keyword AND a nearby password input. + +### Credential capture without a `
` + +Many login UIs never use a `` — the ASUS router admin page submits with +`
Sign In
`, so no `submit` event +is ever dispatched and the save-credentials banner never appeared. + +`watchSubmissions()` therefore registers three triggers, all routed through +`maybeCaptureCredentials(scope)`: + +1. `submit` on any form (scope = the form) +2. `click` on anything `_looksLikeSubmitControl()` accepts — `