docs(pam): clarify safe_user contract — pre-filter, not authoritative boundary
x..y is correctly NOT rejected by the slash/dot/empty/NUL guard; the recipe now documents this as an intentional regression test against over-rejection. Frames canonicalize-then-verify as Phase 17 follow-up hardening.
This commit is contained in:
@@ -38,15 +38,34 @@ pamtester -v authforge "$USER" authenticate
|
||||
|
||||
## Smoke test 3: path-traversal regression
|
||||
|
||||
These usernames must all be rejected with a logged error and `PAM_AUTH_ERR`:
|
||||
The guard in `safe_user` rejects names that break the path-as-identifier
|
||||
invariant: empty strings, `.`, `..`, anything containing `/` or NUL.
|
||||
Anything else passes through — including unusual but legitimate names that
|
||||
contain `..` as a substring (LDAP / federated identities can produce
|
||||
these). The loop below mixes both classes; the assertions live in the
|
||||
comments:
|
||||
|
||||
```bash
|
||||
for u in "../etc" "a/b" "" "." ".." "x..y"; do
|
||||
pamtester -v authforge "$u" authenticate 2>&1 | head -3
|
||||
echo "---"
|
||||
done
|
||||
# Expect rejected (logged "rejected unsafe username"): ../etc, a/b, "", ., ..
|
||||
# Expect passed through (no rejection log; pam_unix runs and prompts): x..y
|
||||
```
|
||||
|
||||
Keeping `x..y` in the loop is intentional — it documents the boundary and
|
||||
acts as a regression test against accidental over-rejection if the guard
|
||||
is ever tightened.
|
||||
|
||||
**Future hardening (deferred — not blocking v1):** the guard is a cheap
|
||||
pre-filter, not the authoritative boundary. A belt-and-suspenders pattern
|
||||
would be canonicalize-then-verify: build the per-user path, resolve it
|
||||
via `fs::canonicalize` (Rust) / `realpath` (C), and confirm the resolved
|
||||
path is still under the expected parent directory. That covers symlink
|
||||
shenanigans and any future `..`-substring attack surface uniformly.
|
||||
Track as a Phase 17 item.
|
||||
|
||||
The module logs via `pam_syslog` without setting its own `openlog` ident,
|
||||
so journal entries inherit the calling process's syslog identifier:
|
||||
`pamtester` here, `sudo` / `sshd` / `gdm` / `login` in production. The
|
||||
|
||||
Reference in New Issue
Block a user