Security Audit Report: Apple Password Manager Resources
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
| Metric | Value |
|---|---|
| Repository | apple/password-manager-resources |
| Commit | e2e0a88 |
| File Analyzed | tools/PasswordRulesParser.js (601 lines) |
| Exploit Candidates Found | 73 |
| Verified Exploits | 22 (4 distinct root causes) |
| Rejected (False Positives) | 15 |
Severity Breakdown
| Severity | Count |
|---|---|
| Critical | 0 |
| High | 0 |
| Medium | 1 |
| Low | 3 |
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 (" → "). 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:
// tools/PasswordRulesParser.js, line 69
toHTMLString() { return `[${this._characters.join("").replace(/"/g, """)}]`; }
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 viainnerHTML, the attacker achieves stored XSS in the password manager context — potentially stealing credentials, session tokens, or vault contents.
Attack Scenario:
- Attacker crafts a password rule:
required: [<img src=x onerror=alert(1)>] - 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. toHTMLString()returns[<img src=x onerror=alert(1)>]with<and>unescaped.- A consuming password manager UI renders this via
innerHTML— the<img>tag executes JavaScript.
Proof of Concept
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: [<>&'"]
// 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
--- 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, """)}]`; }
+ toHTMLString() {
+ const escaped = this._characters.join("")
+ .replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ 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:
// 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: 99999999999999999parsed as100000000000000000) could produce passwords of unexpected length.
Attack Scenario:
- Website serves
minlength: <310-digit number>in its password rules. _parseIntegerreturnsInfinity.parsePasswordRulessetsmaximumMinLength = Math.max(Infinity, 0) = Infinity.- A password generator trying to create a password of length
Infinitycrashes or hangs.
Proof of Concept
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
--- 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:
// 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
}
// Line 455 — same pattern for MIN_LENGTH / MAX_LENGTH
if (propertyValue) { ... }
// 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: 0in 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
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
--- 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
// 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
// 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
TypeErrorpropagates 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
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
--- 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:
| Category | Count | Typical Reason |
|---|---|---|
| Bypasses public API | 9 | PoC directly constructs internal objects (via eval()) with invalid data, bypassing _parseCustomCharacterClass which already filters to ASCII printable range |
| Known design limitation | 2 | - in character classes is intentionally ignored (has FIXME comment + console.warn) |
| Linear complexity is standard | 2 | O(n) parser time is the theoretical minimum; not a vulnerability |
| No security impact | 1 | Array type coercion produces identical output to string input |
| Private function with maintained invariants | 1 | All callers properly maintain position bounds; out-of-bounds only reachable with fabricated arguments |
Notable well-reasoned rejections:
-
_bitSetIndexForCharacterwith negative indices (3 rejections): The verifier correctly identified that_parseCustomCharacterClassalready filters all characters to ASCII printable range (code points 32-126) via_isASCIIPrintableCharacter()before they reach_bitSetIndexForCharacter. The PoC bypassed this by constructingCustomCharacterClassdirectly witheval(), 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.toHTMLStringinjection (1 rejection): Through the public API, only safe hardcoded identifier strings from theIdentifierenum ("upper","lower","digit", etc.) can reachNamedCharacterClass. 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:
- Setup: Repository cloned, dependencies installed, JavaScript validated
- Static Analysis: Dependency graph built, entry points identified in the 601-line parser
- Invariant Discovery: 23 security properties extracted from code patterns across 20 campaigns
- Mission Dispatch: 58 missions targeting HTML output encoding, numeric parsing, input validation, resource exhaustion, and character class handling
- Exploit Generation: Node.js/Jest PoC tests written for each candidate
- Verification: Tests executed against the real
PasswordRulesParser.js— only passing tests qualify - 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 forinnerHTMLinsertion. 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



