Security Audit — Self-Review (Claude, 2026-02-27)

See: security_audits index

3 findings: 0 Critical, 0 High, 1 Medium, 2 Low. All three fixed. Manual review of the code after two external audits, looking for anything the models might have missed.

Finding A (Medium) — socketpair missing SOCK_CLOEXEC

The initial socketpair() call used SOCK_STREAM without SOCK_CLOEXEC.

Impact: if pwsafe's GUI spawned any child process — a browser for help, a file manager, an external editor — that child inherited sv[0] (the parent's socket end). If that child outlived pwsafe, the lock daemon would see the socket stay open even after pwsafe exited, so it would never get EOF, never call child_unlock_all(), and never release the server lock. The server lock would remain held until it expired (WebDAV timeout, typically 5 minutes).

Fix: socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv).

This finding was missed by both external models, which focused more on the IPC protocol and memory safety rather than fd inheritance.

Finding B (Low) — header_cb DAV: check half-case-insensitive

The previous fix for Lock-Token: used strncasecmp, but the DAV: check used a mixed approach: line[0] == 'D' || 'd' (note: this is actually always true — 'd' is truthy), then line.substr(0,4) == "DAV:" (case-sensitive).

Effect: "Dav:" or "dav:" would pass the first check but fail the substr comparison. Apache always emits DAV: in uppercase, so this was latent rather than active.

Fix: strncasecmp(line.c_str(), "DAV:", 4) == 0 for both the header name check and the Lock-Token check.

Finding C (Low) — Parent URL guard inconsistent with child limit

pws_lockd_release() checked ulen > 65535 before sending. The child's recv_string rejected anything > MAX_IPC_URL (8192).

Impact: a URL between 8193 and 65535 bytes (unusual but possible) would pass the parent's guard, be sent in full, be immediately rejected by the child, and cause the daemon to call _exit(1). The daemon would exit without calling child_unlock_all(), silently failing to unlock the server lock. The parent would then get EIO on the next read() — no recovery, unrecoverable state.

Fix: parent guard changed to if (ulen > MAX_IPC_URL) return; — same constant, consistent on both sides.

version1
created2026-02-27