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
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/policy.test.ts+17 −0src/content/policy.ts+49 −9
manifest.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": {

view file

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",

view file

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",

view file

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   });

view file

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 {

view file