317f2df9a402
fix: bound password-policy DOM extraction (PB-26-703, v5.2.10)
- time
- 2026-09-07 01:10 UTC · 1 month ago
- machine
- koh2
- agent
- gpt-5
- session
- red team remediation
- parent
- 6982d0beb2da
↓ peach-browser-317f2df9.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/policy.test.ts+17 −0src/content/policy.ts+49 −9manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.2.9",4+ "version": "5.2.10",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -2,7 +2,7 @@22 "manifest_version": 3,33 "minimum_chrome_version": "116",44 "name": "Peach Password Manager",5- "version": "5.2.9",5+ "version": "5.2.10",66 "description": "Zero-knowledge password manager",77 "action": {88 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "5.2.9",3+ "version": "5.2.10",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/content/policy.test.ts+17 −0
@@ -4,6 +4,23 @@44 import { detectPasswordPolicy, extractNearbyText } from "./policy";55 66 describe("detectPasswordPolicy", () => {7+ it("PB-26-703 bounds hostile nearby DOM before joining text", () => {8+ const input = document.createElement("input");9+ input.type = "password";10+ input.title = "x".repeat(20_000);11+ const wrapper = document.createElement("div");12+ wrapper.append(input);13+ for (let i = 0; i < 1_000; i += 1) {14+ const hint = document.createElement("span");15+ hint.textContent = "y".repeat(20_000);16+ wrapper.append(hint);17+ }18+ document.body.append(wrapper);19+20+ const text = extractNearbyText(input);21+ expect(text.length).toBeLessThanOrEqual(4_096);22+ });23+724 beforeEach(() => {825 document.body.innerHTML = "";926 });src/content/policy.ts+49 −9
@@ -9,6 +9,11 @@99 import type { PasswordPolicy } from "../shared/types";1010 1111 const NEGATION_WINDOW = 220;12+const MAX_POLICY_TEXT_CHARS = 4_096;13+const MAX_POLICY_PART_CHARS = 1_024;14+const MAX_DESCRIBED_IDS = 16;15+const MAX_HINTS = 32;16+const MAX_SCANNED_NODES = 256;1217 1318 // Characters that plausibly appear in a site's forbidden-character list.1419 // Restricted to ASCII punctuation a user could actually type. Anything else@@ -22,21 +27,46 @@2227 export function extractNearbyText(el: HTMLInputElement | null): string {2328 if (!el) return "";2429 const parts: string[] = [];30+ let remaining = MAX_POLICY_TEXT_CHARS;31+ const append = (text: string | null | undefined) => {32+ const separator = parts.length > 0 ? 1 : 0;33+ const length = Math.min(MAX_POLICY_PART_CHARS, remaining - separator);34+ if (!text || length <= 0) return;35+ parts.push(text.slice(0, length));36+ remaining -= Math.min(text.length, length) + separator;37+ };38+ const boundedText = (root: Element): string => {39+ const text: string[] = [];40+ let chars = 0;41+ let visited = 0;42+ const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_ALL);43+ let current = walker.nextNode();44+ while (visited < MAX_SCANNED_NODES && chars < MAX_POLICY_PART_CHARS && current) {45+ visited += 1;46+ if (current.nodeType === Node.TEXT_NODE && current.nodeValue) {47+ const value = current.nodeValue.slice(0, MAX_POLICY_PART_CHARS - chars);48+ text.push(value);49+ chars += value.length;50+ }51+ current = walker.nextNode();52+ }53+ return text.join(" ");54+ };2555 2656 // aria-describedby may be a space-separated list of IDs2757 const describedBy = el.getAttribute("aria-describedby");2858 if (describedBy) {29- for (const id of describedBy.trim().split(/\s+/)) {59+ for (const id of describedBy.trim().split(/\s+/).slice(0, MAX_DESCRIBED_IDS)) {3060 const desc = el.ownerDocument.getElementById(id);31- if (desc?.textContent) parts.push(desc.textContent);61+ if (desc) append(boundedText(desc));3262 }3363 }3464 3565 // Field-level attributes that sometimes carry requirements36- if (el.title) parts.push(el.title);37- if (el.placeholder) parts.push(el.placeholder);66+ append(el.title);67+ append(el.placeholder);3868 const ariaLabel = el.getAttribute("aria-label");39- if (ariaLabel) parts.push(ariaLabel);69+ append(ariaLabel);4070 4171 const HINT_SELECTOR =4272 "p, span, li, small, label, div, .hint, .requirement, .password-rule, .password-hint, .password-rules, .field-hint, .helper-text, .input-hint";@@ -45,14 +75,24 @@4575 for (let i = 0; i < 5; i++) {4676 node = node?.parentElement ?? null;4777 if (!node) break;48- const hints = node.querySelectorAll(HINT_SELECTOR);49- if (hints.length > 0) {50- parts.push(Array.from(hints).map((h) => h.textContent).join(" "));78+ const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);79+ let current = walker.nextNode();80+ let visited = 0;81+ let hints = 0;82+ while (visited < MAX_SCANNED_NODES && hints < MAX_HINTS && current) {83+ visited += 1;84+ if (current instanceof Element && current.matches(HINT_SELECTOR)) {85+ append(boundedText(current));86+ hints += 1;87+ }88+ current = walker.nextNode();89+ }90+ if (hints > 0) {5191 break;5292 }5393 }5494 55- return parts.filter(Boolean).join(" ");95+ return parts.join(" ");5696 }5797 5898 export function detectPasswordPolicy(doc: Document = document): PasswordPolicy {