PAM - Security

<- Back to PAM


PAM Security Model

This document describes the security model for PAM, the known risks, and the mitigations in place.


Threat model

PAM is a client-side web application. All encryption and decryption happens in the browser using the Web Crypto API (SubtleCrypto). The encrypted file is stored locally by the user.

Your records are never sent to a server. There is exactly one feature that contacts anything at all, it is off by default, and it does not transmit your data:

As of v2.5.0 each saved file also carries a SHA-256 digest of its records and preferences (meta.integrity), inside the encrypted payload. It is verified on load before any content or preference is applied, and a file that fails is refused. This is tamper evidence, not authenticated encryption: it is checked after decryption rather than before, the digest is unkeyed and could be stripped by anyone able to rewrite the file, and it does not detect rollback to a genuine older copy. AES-GCM remains the correct fix and requires a format change.

The Content-Security-Policy in www/index.html names that one host in connect-src and permits no other, so what PAM is able to contact is verifiable from the page source rather than from this document. Note that the policy permits the host whether or not the preference is enabled: a <meta> policy is fixed when the page is parsed and cannot depend on a runtime setting, and multiple policies compose by intersection, so a second one could only tighten the first. The preference controls whether PAM makes the request, not whether it could.

The primary threat vectors are:

  1. Compromise of the encrypted file — an attacker obtains the .pam file and attempts to brute-force the master password offline.
  2. XSS via malicious file content — a user is tricked into loading a PAM file from an untrusted source that contains malicious HTML in an html-type field.
  3. Master password exposure — the master password is cached in browser storage and exposed via XSS, a compromised extension, or physical access to the device.

SEC-001 — HTML field rendering

PAM supports an html field type that allows rich content (formatted notes, links, instructions). This is an intentional feature designed for controlled internal deployments where a security team authors PAM data and distributes it on a read-only volume to a defined cohort.

Risk: If a user loads a PAM file from an untrusted source, html fields could contain hostile markup. Historically this was described as an XSS risk. Since v2.4.0 the Content-Security-Policy makes script execution the less likely outcome; the live risk is content injection. Both are covered below.

Mitigation: - HTML field rendering is disabled by default. Fields of type html render as escaped plain text with a </> badge. - The preference is on the Administration tab of Preferences, reachable only after unlocking preferences with the prefs password. - When HTML rendering is enabled, a persistent ⚠ HTML ON warning badge is shown in the toolbar so users always know they are in a mode where HTML fields render live. - The Content-Security-Policy in www/index.html blocks the usual script paths. script-src carries no 'unsafe-inline', so an injected <img onerror=…> or javascript: URL does not run, and markup inserted through innerHTML never executes <script> tags regardless. Exfiltration is constrained too: img-src 'self' data: blocks the classic beacon, style-src 'self' blocks CSS-based leaks, and connect-src permits only PAM’s own origin and the HIBP range API.

The preference is not only reachable through the Preferences dialogue. A loaded PAM file carries a prefs block, and PAM applies it. A file can therefore switch HTML rendering on by itself. The ⚠ HTML ON badge appears when it does, but it reports the new state rather than asking first. See Residual risk below.

When is it safe to enable? - The PAM file was authored by a trusted party (e.g. your own security team). - The file is distributed on a read-only volume and cannot be modified by end users. - You are not loading PAM files from untrusted URLs.

Residual risk. Three items, in decreasing order of concern:

  1. Content injection, not script execution. Injected markup still renders. It can present convincing interface elements — a plausible confirm your master password prompt, for example — inside a page the user already trusts. The CSP does not prevent this, because nothing about it requires script.
  2. Form submission is constrained as of v2.5.0. form-action 'self' is now in the policy. It was previously absent, and unlike most directives form-action does not fall back to default-src — so a form in injected markup could submit to any origin. That was the one exfiltration channel the rest of the policy left open. A unit test now asserts the directive is present, because its absence is otherwise completely silent.
  3. A distributed file carries its own setting. A PAM file shipped with HTML rendering already enabled in its prefs block turns it on for whoever loads it. The mitigations above protect against foreign malicious files; they do not protect against a trusted file that has been tampered with, nor against a user loading a file whose provenance they have not checked.

Note on the CSP. It is a strong mitigation and not a guarantee. script-src allows https://cdn.jsdelivr.net for the Bootstrap bundle, and that host serves arbitrary npm and GitHub content; the allowance is safe here only because markup inserted via innerHTML cannot execute script tags at all. Do not treat the CSP as licence to relax the default.


SEC-002 — Master password caching

The master file password can be cached in browser storage to avoid re-entry on every save/load. The cache strategy is controlled in Preferences → Administration → filePass Cache Strategy.

Options: - session (default) — password is cached in sessionStorage for the current browser tab only and cleared when the tab is closed. - local — password persists in localStorage across sessions and power cycles. Use with caution on shared devices. A ⚠ PASS: LOCAL warning badge appears in the toolbar while this is active. - global — password is held in a JavaScript variable only (lost on page reload). - none — password is never cached; must be re-entered on every load/save.

Per-device strategy persistence:

The chosen strategy is stored per-device in localStorage under the key pamCacheStrategy. On startup, PAM reads this key and applies it before loading any file, solving the chicken-and-egg problem where the PAM file (which contains preferences) cannot be opened without first knowing the cache strategy.

This means the user’s strategy choice persists across page reloads, browser restarts, and power cycles, independently of the PAM file. It can be changed at any time in Preferences and takes effect immediately. When the strategy is changed, the password is cleared from the previous storage location before the new strategy is applied.

PWA note:

When PAM is installed as a Progressive Web App (home screen shortcut) on iOS Safari, each launch may start a fresh browser session, wiping sessionStorage. If your PAM file has filePassCache set to local in its preferences, PAM now correctly detects this after decrypting the file and re-stores the password in localStorage, persisting the strategy as pamCacheStrategy so subsequent launches use the same bucket. (BUG-002)

History: - v2.0.3 — default changed from local to session as a security improvement (SEC-002). - v2.0.5 — default reverted to local due to PWA reload behaviour causing excessive re-entry friction; ⚠ PASS: LOCAL badge added as compensating control. - v2.0.7 — default changed back to session; per-device pamCacheStrategy persistence introduced so the user’s choice survives reloads without being tied to the PAM file default. - v1.3.1 — BUG-002 fixed: password was stored in the wrong storage bucket when the loaded file’s filePassCache pref differed from the startup default. Manifested as lost password on iOS PWA relaunch when the file specified local but the device had no prior pamCacheStrategy set.

Recommendation: Use session (the default) for most environments. Use local only on a trusted personal device where re-entry friction is a genuine concern.


SEC-003 / SEC-004 — Encryption format (v1 vs v2)

PAM v1.x uses AES-256-CBC with PBKDF2-SHA-256 key derivation. The v1 implementation has two known weaknesses:

  1. Low iteration count (100,000) — NIST SP 800-132 (2023) and OWASP now recommend ≥ 600,000 iterations.
  2. Salt entropy bug — the random salt bytes are passed through TextEncoder.encode() which calls .toString() first, producing an ASCII string like "0,34,211,..." instead of raw bytes, dramatically reducing salt entropy.

Both bugs define the key derivation for all existing v1 files. Fixing them requires a new file format (v2).

Migration: v2 format support is planned for PAM v1.3. The migration is non-breaking — v1 files can always be decrypted; users choose when to re-save in v2 format. See MIGRATION.md (published with v2.0).


SEC-006 — Preferences lock password

The preferences lock password (lockPreferencesPassword) is stored as plaintext in the PAM file’s prefs block.

This is intentional. The prefs lock is a convenience feature whose sole purpose is to prevent casual or accidental modification of preferences in a shared deployment. It is not a cryptographic boundary.

The entire PAM file — including the prefs block and lockPreferencesPassword — is encrypted with the master password (AES-256-CBC). An attacker who has the master password can read everything. Hashing lockPreferencesPassword would add no security benefit in this threat model, and would silently break existing files where the value was stored as plaintext.

Threat model: The prefs lock protects against an authorised user (who has the master password) accidentally changing deployment-managed settings. It does not protect against a determined attacker.


SEC-007 — URL load validation

The Load → URL feature only accepts https:// and http:// URLs. javascript:, data:, ftp:, and other URI schemes are rejected to prevent protocol-based injection attacks.


SEC-008 — Content Security Policy

PAM sets a Content-Security-Policy meta tag in index.html:

default-src 'self';
script-src 'self' https://cdn.jsdelivr.net;
style-src 'self';
img-src 'self' data:;
font-src 'self'

This prevents loading of scripts, styles, and resources from untrusted origins. The cdn.jsdelivr.net exception covers the DOMPurify library used for HTML sanitization.


Reporting security issues

This is an open-source personal password manager. If you find a security issue please open a GitHub issue at jlinoff/pam.