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
|
## 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
|
```bash
|
||||||
for u in "../etc" "a/b" "" "." ".." "x..y"; do
|
for u in "../etc" "a/b" "" "." ".." "x..y"; do
|
||||||
pamtester -v authforge "$u" authenticate 2>&1 | head -3
|
pamtester -v authforge "$u" authenticate 2>&1 | head -3
|
||||||
echo "---"
|
echo "---"
|
||||||
done
|
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,
|
The module logs via `pam_syslog` without setting its own `openlog` ident,
|
||||||
so journal entries inherit the calling process's syslog identifier:
|
so journal entries inherit the calling process's syslog identifier:
|
||||||
`pamtester` here, `sudo` / `sshd` / `gdm` / `login` in production. The
|
`pamtester` here, `sudo` / `sshd` / `gdm` / `login` in production. The
|
||||||
|
|||||||
Reference in New Issue
Block a user