Security Audit Report: Apple Password Manager Resources

Security Audit Report: Apple Password Manager Resources

By Kai Team12.02.2026

We ran Kai Agent against the password-manager-resources repository, focusing on the tools/PasswordRulesParser.js password rules parser (601 LOC). The automated analysis discovered 73 exploit candidates, verified 22, and rejected 15 with well-articulated reasoning. After deduplication, the 22 verified verdicts map to 4 distinct root causes presented below.

Executive Summary

MetricValue
Repositoryapple/password-manager-resources
Commite2e0a88
File Analyzedtools/PasswordRulesParser.js (601 lines)
Exploit Candidates Found73
Verified Exploits22 (4 distinct root causes)
Rejected (False Positives)15

Severity Breakdown

SeverityCount
Critical0
High0
Medium1
Low3

Verified Vulnerabilities

[MEDIUM-1] Incomplete HTML Entity Encoding in CustomCharacterClass.toHTMLString() — XSS

Severity: Medium

Affected File: tools/PasswordRulesParser.js

Affected Function: CustomCharacterClass.toHTMLString() (line 69)

Vulnerability Class: Cross-Site Scripting / Incomplete Output Encoding (CWE-79)

Verdicts: 8 (across 5 invariants — same root cause)

Description

CustomCharacterClass.toHTMLString() generates HTML output from parsed password rule characters but only escapes double quotes ("&quot;). The HTML metacharacters <, >, &, and ' pass through unescaped. If a consuming application renders this output via innerHTML or similar DOM sinks, an attacker who controls the password rules string can inject arbitrary HTML/JavaScript.

Root Cause Analysis

The toHTMLString() method on line 69 applies a single replacement:

javascript
// tools/PasswordRulesParser.js, line 69
toHTMLString() { return `[${this._characters.join("").replace(/"/g, "&quot;")}]`; }

The method name toHTMLString implies the output is safe for HTML contexts, but only " is escaped. Characters < (0x3C), > (0x3E), & (0x26), and ' (0x27) are all within the ASCII printable range (0x20–0x7E) that _parseCustomCharacterClass accepts (see the _isASCIIPrintableCharacter check on line 322), so they flow through the public API into toHTMLString() unmodified.

Impact

  • Attack Vector: A website operator (or an attacker who can modify password rules served to a password manager) crafts a rule containing HTML metacharacters in a custom character class.
  • Potential Loss: If a password manager UI renders toHTMLString() output via innerHTML, the attacker achieves stored XSS in the password manager context — potentially stealing credentials, session tokens, or vault contents.

Attack Scenario:

  1. Attacker crafts a password rule: required: [<img src=x onerror=alert(1)>]
  2. The parser accepts <, i, m, g, , s, r, c, =, x, o, n, e, r, r, o, r, =, a, l, e, r, t, (, 1, ), > — all ASCII printable characters.
  3. toHTMLString() returns [<img src=x onerror=alert(1)>] with < and > unescaped.
  4. A consuming password manager UI renders this via innerHTML — the <img> tag executes JavaScript.

Proof of Concept

javascript
const { parsePasswordRules } = require('./tools/PasswordRulesParser.js');

// Parse a rule with HTML metacharacters in a custom character class
const rules = parsePasswordRules('required: [<>&\'"]');

// Find the CustomCharacterClass in the output
const allowedRule = rules.find(r => r.name === 'allowed');
const customClass = allowedRule.value.find(v => v.characters);

const htmlOutput = customClass.toHTMLString();

// Only " is escaped — <, >, &, ' pass through raw
console.log(htmlOutput);
// Output: [<>&'&quot;]

// Verify: < and > are present unescaped
console.assert(htmlOutput.includes('<'), 'Unescaped < present');
console.assert(htmlOutput.includes('>'), 'Unescaped > present');
console.assert(htmlOutput.includes('&\''), 'Unescaped & and \' present');
console.assert(!htmlOutput.includes('"'), '" is escaped');

Recommended Fix

diff
--- tools/PasswordRulesParser.js
+++ tools/PasswordRulesParser.js
@@ -66,7 +66,15 @@
     }
     get characters() { return this._characters; }
     toString() { return `[${this._characters.join("")}]`; }
-    toHTMLString() { return `[${this._characters.join("").replace(/"/g, "&quot;")}]`; }
+    toHTMLString() {
+        const escaped = this._characters.join("")
+            .replace(/&/g, "&amp;")
+            .replace(/</g, "&lt;")
+            .replace(/>/g, "&gt;")
+            .replace(/"/g, "&quot;")
+            .replace(/'/g, "&#x27;");
+        return `[${escaped}]`;
+    }
 };

[LOW-1] Unbounded Integer Accumulation in _parseInteger() — Infinity / Precision Loss

Severity: Low

Affected File: tools/PasswordRulesParser.js

Affected Functions: _parseInteger() (lines 474-498), _parseMinLengthMaxLengthPropertyValue() (lines 464-467), _parseMaxConsecutivePropertyValue() (lines 469-472)

Vulnerability Class: Integer Overflow / Numeric Validation (CWE-190)

Verdicts: 6 (across 3 invariants — same root cause)

Description

_parseInteger() accumulates digits via result = 10 * result + parseInt(...) with no bounds checking. This produces three distinct failure modes: (1) a 310+ digit number overflows IEEE 754 to Infinity, (2) a 17+ digit number exceeds Number.MAX_SAFE_INTEGER causing precision loss, and (3) zero is accepted as a valid value despite being semantically invalid for password length constraints.

Root Cause Analysis

The parsing loop at lines 486-490 accumulates without any validation:

javascript
// tools/PasswordRulesParser.js, lines 486-493
let result = 0;
do {
    result = 10 * result + parseInt(input[position], 10);
    ++position;
} while (position < length && input[position] !== PROPERTY_SEPARATOR && _isASCIIDigit(input[position]));

if (position >= length || input[position] === PROPERTY_SEPARATOR) {
    return [result, position];  // No validation — returns Infinity, unsafe integers, or 0
}

The passthrough wrappers _parseMinLengthMaxLengthPropertyValue and _parseMaxConsecutivePropertyValue add no validation of their own.

Impact

  • Attack Vector: A website operator provides a password rule with an excessively large integer: minlength: 999...999 (310+ digits).
  • Potential Loss: Downstream password generators consuming these rules could attempt to generate a password of length Infinity, enter infinite loops, or make unbounded memory allocations. Precision-loss values (e.g., minlength: 99999999999999999 parsed as 100000000000000000) could produce passwords of unexpected length.

Attack Scenario:

  1. Website serves minlength: <310-digit number> in its password rules.
  2. _parseInteger returns Infinity.
  3. parsePasswordRules sets maximumMinLength = Math.max(Infinity, 0) = Infinity.
  4. A password generator trying to create a password of length Infinity crashes or hangs.

Proof of Concept

javascript
const { parsePasswordRules } = require('./tools/PasswordRulesParser.js');

// 310-digit number → Infinity
const hugeNumber = '9'.repeat(310);
const rules1 = parsePasswordRules(`minlength: ${hugeNumber};`);
const minLenRule = rules1.find(r => r.name === 'minlength');
console.assert(minLenRule.value === Infinity, 'Parsed as Infinity');

// 20-digit number → precision loss
const rules2 = parsePasswordRules('minlength: 99999999999999999999;');
const minLen2 = rules2.find(r => r.name === 'minlength');
console.assert(minLen2.value > Number.MAX_SAFE_INTEGER, 'Exceeds safe integer');
console.assert(!Number.isSafeInteger(minLen2.value), 'Precision lost');

Recommended Fix

diff
--- tools/PasswordRulesParser.js
+++ tools/PasswordRulesParser.js
@@ -463,12 +463,38 @@

 function _parseMinLengthMaxLengthPropertyValue(input, position)
 {
-    return _parseInteger(input, position);
+    let [value, newPosition] = _parseInteger(input, position);
+    if (value !== null) {
+        if (!Number.isFinite(value) || !Number.isSafeInteger(value) || value <= 0) {
+            console.error("Invalid minlength/maxlength value: must be a positive safe integer");
+            return [null, newPosition];
+        }
+    }
+    return [value, newPosition];
 }

 function _parseMaxConsecutivePropertyValue(input, position)
 {
-    return _parseInteger(input, position);
+    let [value, newPosition] = _parseInteger(input, position);
+    if (value !== null) {
+        if (!Number.isFinite(value) || !Number.isSafeInteger(value) || value <= 0) {
+            console.error("Invalid max-consecutive value: must be a positive safe integer");
+            return [null, newPosition];
+        }
+    }
+    return [value, newPosition];
 }

[LOW-2] max-consecutive: 0 Silently Dropped by Falsy Check

Severity: Low

Affected File: tools/PasswordRulesParser.js

Affected Functions: _parsePasswordRule() (lines 447, 455), _parsePasswordRulesInternal() (line 513)

Vulnerability Class: Input Validation / Logic Error (CWE-20)

Verdicts: 3 (across 2 invariants — same root cause)

Description

The parser uses JavaScript falsy checks (if (propertyValue)) to distinguish between a successfully parsed value and a parse error (null). Since 0 is falsy in JavaScript, a successfully parsed max-consecutive: 0, minlength: 0, or maxlength: 0 is treated identically to a parse error — the rule is silently dropped with no error message.

Root Cause Analysis

Three locations use the same falsy-check pattern:

javascript
// tools/PasswordRulesParser.js, line 447
case RuleName.MAX_CONSECUTIVE: {
    var [propertyValue, position] = _parseMaxConsecutivePropertyValue(input, position);
    if (propertyValue) {        // ← 0 is falsy, so value=0 is treated as parse error
        property.value = propertyValue;
    }
    return [new Rule(property.name, property.value), position];  // property.value stays null
}
javascript
// Line 455 — same pattern for MIN_LENGTH / MAX_LENGTH
if (propertyValue) { ... }
javascript
// Line 513 — final filter also uses falsy check
if (parsedProperty && parsedProperty.value) {  // value=null → rule dropped entirely
    parsedProperties.push(parsedProperty);
}

The correct check should be propertyValue !== null to distinguish "parsed 0" from "parse error".

Impact

  • Attack Vector: Website serves max-consecutive: 0 in password rules.
  • Potential Loss: The rule is silently ignored. A password generator that should enforce "no consecutive characters" (a degenerate but explicit constraint) instead applies no consecutive-character restriction at all. The silent failure makes the misconfiguration invisible to the rule author.

Note: While max-consecutive: 0 is semantically degenerate (no character can appear even once), the bug also affects minlength: 0 and maxlength: 0. The core issue is the incorrect conflation of "successfully parsed zero" with "parse error."

Proof of Concept

javascript
const { parsePasswordRules } = require('./tools/PasswordRulesParser.js');

// max-consecutive: 0 is parsed correctly by _parseInteger (returns [0, ...])
// but silently dropped by the falsy check in _parsePasswordRule
const rules = parsePasswordRules('max-consecutive: 0;');
const maxConsec = rules.find(r => r.name === 'max-consecutive');
console.assert(maxConsec === undefined, 'max-consecutive: 0 was silently dropped');

// Compare with max-consecutive: 1 — works fine
const rules2 = parsePasswordRules('max-consecutive: 1;');
const maxConsec2 = rules2.find(r => r.name === 'max-consecutive');
console.assert(maxConsec2 !== undefined, 'max-consecutive: 1 is preserved');
console.assert(maxConsec2.value === 1);

Recommended Fix

diff
--- tools/PasswordRulesParser.js
+++ tools/PasswordRulesParser.js
@@ -444,7 +444,7 @@
         case RuleName.MAX_CONSECUTIVE: {
             var [propertyValue, position] = _parseMaxConsecutivePropertyValue(input, position);
-            if (propertyValue) {
+            if (propertyValue !== null) {
                 property.value = propertyValue;
             }
             return [new Rule(property.name, property.value), position];
@@ -452,7 +452,7 @@
         case RuleName.MIN_LENGTH:
         case RuleName.MAX_LENGTH: {
             var [propertyValue, position] = _parseMinLengthMaxLengthPropertyValue(input, position);
-            if (propertyValue) {
+            if (propertyValue !== null) {
                 property.value = propertyValue;
             }
             return [new Rule(property.name, property.value), position];
@@ -510,7 +510,7 @@

         var [parsedProperty, position] = _parsePasswordRule(input, position)
-        if (parsedProperty && parsedProperty.value) {
+        if (parsedProperty && parsedProperty.value !== null) {
             parsedProperties.push(parsedProperty);
         }

[LOW-3] Missing Input Type Validation in parsePasswordRules()

Severity: Low

Affected File: tools/PasswordRulesParser.js

Affected Function: parsePasswordRules() (line 538)

Vulnerability Class: Missing Input Validation / Unhandled Exception (CWE-20, CWE-755)

Verdicts: 2 (across 2 invariants — same root cause)

Description

The public entry point parsePasswordRules() passes its input parameter directly to _parsePasswordRulesInternal() without any type validation. When called with null, undefined, or non-string types, the internal function immediately accesses input.length (line 503), throwing an unhandled TypeError.

Root Cause Analysis

javascript
// tools/PasswordRulesParser.js, lines 538-540
function parsePasswordRules(input, formatRulesForMinifiedVersion)
{
    let passwordRules = _parsePasswordRulesInternal(input) || [];
    // ↑ _parsePasswordRulesInternal accesses input.length on line 503
    //   with no type guard — throws TypeError for null/undefined
javascript
// tools/PasswordRulesParser.js, lines 500-503
function _parsePasswordRulesInternal(input)
{
    let parsedProperties = [];
    let length = input.length;  // ← TypeError: Cannot read properties of null

Impact

  • Attack Vector: Caller passes null, undefined, or non-string value.
  • Potential Loss: Unhandled TypeError propagates to the calling application. In a web context, this could crash a password manager UI or cause a denial of service for the password generation feature.

Note: This is a standard defensive programming issue. The function's implicit contract is "pass a string," but a well-designed public API should validate its inputs rather than crashing.

Proof of Concept

javascript
const { parsePasswordRules } = require('./tools/PasswordRulesParser.js');

// Both throw TypeError: Cannot read properties of null/undefined (reading 'length')
try {
    parsePasswordRules(null);
    console.assert(false, 'Should have thrown');
} catch (e) {
    console.assert(e instanceof TypeError);
}

try {
    parsePasswordRules(undefined);
    console.assert(false, 'Should have thrown');
} catch (e) {
    console.assert(e instanceof TypeError);
}

Recommended Fix

diff
--- tools/PasswordRulesParser.js
+++ tools/PasswordRulesParser.js
@@ -538,6 +538,11 @@
 function parsePasswordRules(input, formatRulesForMinifiedVersion)
 {
+    // Validate input type — return empty array for non-string inputs
+    if (typeof input !== 'string') {
+        return [];
+    }
+
     let passwordRules = _parsePasswordRulesInternal(input) || [];

Rejected Findings (False Positives)

The following 15 exploit candidates were investigated but rejected:

CategoryCountTypical Reason
Bypasses public API9PoC directly constructs internal objects (via eval()) with invalid data, bypassing _parseCustomCharacterClass which already filters to ASCII printable range
Known design limitation2- in character classes is intentionally ignored (has FIXME comment + console.warn)
Linear complexity is standard2O(n) parser time is the theoretical minimum; not a vulnerability
No security impact1Array type coercion produces identical output to string input
Private function with maintained invariants1All callers properly maintain position bounds; out-of-bounds only reachable with fabricated arguments

Notable well-reasoned rejections:

  • _bitSetIndexForCharacter with negative indices (3 rejections): The verifier correctly identified that _parseCustomCharacterClass already filters all characters to ASCII printable range (code points 32-126) via _isASCIIPrintableCharacter() before they reach _bitSetIndexForCharacter. The PoC bypassed this by constructing CustomCharacterClass directly with eval(), which is not reachable through the public API.

  • DoS via linear parsing (2 rejections): The verifier correctly noted that "linear O(n) time complexity is the theoretical optimum for a parser" and that the missing input length cap "is a standard hardening measure (caller's responsibility), not a parser bug." Password rules are provided by the website operator, not by end users, making the attack vector impractical.

  • NamedCharacterClass.toHTMLString injection (1 rejection): Through the public API, only safe hardcoded identifier strings from the Identifier enum ("upper", "lower", "digit", etc.) can reach NamedCharacterClass. The parser's lexer restricts identifiers to [a-zA-Z-] characters only, making HTML injection impossible.

Methodology

Kai Agent uses a multi-phase security analysis pipeline:

  1. Setup: Repository cloned, dependencies installed, JavaScript validated
  2. Static Analysis: Dependency graph built, entry points identified in the 601-line parser
  3. Invariant Discovery: 23 security properties extracted from code patterns across 20 campaigns
  4. Mission Dispatch: 58 missions targeting HTML output encoding, numeric parsing, input validation, resource exhaustion, and character class handling
  5. Exploit Generation: Node.js/Jest PoC tests written for each candidate
  6. Verification: Tests executed against the real PasswordRulesParser.js — only passing tests qualify
  7. Fix Generation: 8 patches generated targeting all 4 root causes

Verification Against Original Code

All vulnerabilities were verified against the original password-manager-resources repository at commit e2e0a88:

  • File compared: tools/PasswordRulesParser.js
  • Result: IDENTICAL — no modifications between test repository and upstream
  • Confidence: All vulnerabilities exist in the production source code

Conclusion

Apple's PasswordRulesParser.js is a well-structured, compact parser with clear function boundaries and defensive assertions. However, the analysis uncovered 1 medium-severity XSS vulnerability and 3 low-severity robustness issues:

  • The toHTMLString() method's incomplete HTML entity encoding is the most significant finding. Given that the method's name implies HTML-safety, consuming password manager UIs may reasonably trust its output for innerHTML insertion. Escaping all 5 HTML metacharacters (& < > " ') is the standard fix.

  • The _parseInteger() bounds issue and falsy-check logic errors are defense-in-depth improvements. While the attack surface is narrow (password rules are site-operator-controlled, not user-supplied), unbounded numeric values flowing to downstream password generators could cause unexpected behavior.

  • The verifier demonstrated strong false-positive filtering, correctly rejecting 15 candidates — particularly effective at detecting PoCs that bypass the public API by directly constructing internal objects with invalid data.


Report generated by Kai Agent

Copyright © 2026 DRIA. All Rights Reserved.
Follow Kai: