Aug 26 - Update password detect against off field
CI / Python lint (flake8) (push) Has been cancelled
CI / Python syntax check (push) Has been cancelled
CI / Alembic migration chain (push) Has been cancelled
CI / JavaScript syntax check (push) Has been cancelled
CI / Pytest (push) Has been cancelled
CI / Build extension zip (push) Has been cancelled

This commit is contained in:
2026-08-26 14:47:16 -04:00
parent b84a6d9245
commit 2f1afb143c
4 changed files with 287 additions and 40 deletions
+119
View File
@@ -0,0 +1,119 @@
/**
* tests/js/test_field_heuristics.js — run with:
* node tests/js/test_field_heuristics.js
*
* Pins the field-detection predicates in extension/content/content.js against
* the ASUS RT-AX88U router login page, which the extension could not detect.
*
* The username input there is about as obvious as they come:
*
* <input type="text" id="login_username" name="login_username"
* class="form_input" placeholder="Username" autocomplete="off">
*
* but "off" was in the NON_CRED_AC deny-list, so _isLikelyUsernameField()
* returned false at that check and never reached the name/id/placeholder
* keyword test. `autocomplete="off"` says nothing about whether a field holds a
* credential — routers and admin panels set it specifically to discourage
* password managers.
*
* SCOPE: this exercises the regex predicates extracted from the real source
* file, not the DOM traversal in _hasPasswordSibling() (which needs a browser).
* Those paths are covered by the manual check in the accompanying notes.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const SRC = fs.readFileSync(
path.join(__dirname, '..', '..', 'extension', 'content', 'content.js'),
'utf8',
);
let failures = 0;
function check(label, got, want) {
if (got !== want) {
failures++;
console.log(`FAIL ${label}\n got=${got} want=${want}`);
}
}
/** Pull a named regex literal out of the real source rather than copying it. */
function extractRegex(name) {
const m = SRC.match(new RegExp(`const ${name} =\\s*(/[^\\n]+/[a-z]*);`));
if (!m) throw new Error(`could not find regex ${name} in content.js`);
// eslint-disable-next-line no-eval
return eval(m[1]);
}
const CRED_HINTS = extractRegex('CRED_HINTS');
const NON_CRED_AC = extractRegex('NON_CRED_AC');
// ── The bug: autocomplete="off" must not be a negative signal ───────────────
check('NON_CRED_AC rejects "off"', NON_CRED_AC.test('off'), false);
check('NON_CRED_AC rejects "OFF" (case-insensitive)', NON_CRED_AC.test('OFF'), false);
// Anti-autofill junk values sites use must also fall through to the keywords.
for (const junk of ['nope', 'no', 'false', 'disabled', 'new-user', 'chrome-off']) {
check(`NON_CRED_AC lets junk value "${junk}" fall through`,
NON_CRED_AC.test(junk), false);
}
// ── But genuine non-credential tokens must still be rejected ───────────────
for (const token of ['name', 'given-name', 'family-name', 'organization',
'street-address', 'country', 'postal-code', 'search',
'url', 'bday', 'sex', 'photo', 'language',
'new-password', 'current-password', 'one-time-code']) {
check(`NON_CRED_AC still rejects "${token}"`, NON_CRED_AC.test(token), true);
}
// ── The ASUS field's identifying attributes must read as a credential ───────
const ASUS_USERNAME_ATTRS = ['login_username', 'login_username', 'Username', ''].join(' ');
check('ASUS username attrs match CRED_HINTS',
CRED_HINTS.test(ASUS_USERNAME_ATTRS), true);
// Other real-world username fields.
for (const attrs of ['user_name', 'j_username', 'email', 'userEmail',
'account', 'Login ID', 'mobile', 'tel']) {
check(`CRED_HINTS matches "${attrs}"`, CRED_HINTS.test(attrs), true);
}
// Fields that must NOT be treated as credential inputs on keywords alone.
for (const attrs of ['q', 'search-query', 'first_name', 'zipcode',
'street', 'company', 'comment']) {
check(`CRED_HINTS does not match "${attrs}"`, CRED_HINTS.test(attrs), false);
}
// ── Submit-control detection for pages with no <form> ──────────────────────
// The ASUS page submits via <div class="button" onclick="preLogin();">, so no
// "submit" event ever fires and the save prompt never appeared.
const clsMatch = SRC.match(
/return (\/\(\^\|\[\\s_-\]\)\(btn\|button[^\n]+\/)\.test\(cls\);/,
);
if (!clsMatch) {
failures++;
console.log('FAIL could not find the submit-control class regex in content.js');
} else {
// eslint-disable-next-line no-eval
const BTN_CLS = eval(clsMatch[1]);
check('recognises class="button" (ASUS)', BTN_CLS.test('button'), true);
for (const cls of ['btn', 'btn btn-primary', 'submit', 'login-button',
'signin', 'sign-in', 'form_btn button']) {
check(`recognises class="${cls}"`, BTN_CLS.test(cls), true);
}
for (const cls of ['form_input', 'container', 'buttonish', 'rebutton']) {
check(`ignores class="${cls}"`, BTN_CLS.test(cls), false);
}
}
// The click and keydown fallbacks must actually be registered.
check('click fallback registered', /addEventListener\(\s*"click"/.test(SRC), true);
check('keydown fallback registered', /addEventListener\(\s*"keydown"/.test(SRC), true);
check('submit listener retained', /addEventListener\(\s*"submit"/.test(SRC), true);
if (failures) {
console.log(`\n${failures} failure(s)`);
process.exit(1);
}
console.log('OK: field heuristics assertions passed');