summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHauke Mehrtens2026-05-17 22:01:16 +0000
committerHauke Mehrtens2026-05-20 00:55:10 +0000
commit1b624f8f814ed568608d756512892416e0431d77 (patch)
tree8010408ad8891cbb4df425e6f27d5b5e0efee569
parent6ab9abb56bcb28684f382f7a94c170fad02348ea (diff)
downloaduhttpd-1b624f8f814ed568608d756512892416e0431d77.tar.gz
auth: classify $p$ lookups by account state
uh_auth_add resolves '$p$<user>' password lines by reading the named account's stored credential from /etc/shadow (or /etc/passwd as a fallback). The previous code copied that string verbatim into realm->pass without checking whether it was a usable crypt(3) hash. This broke for two distinct shadow states: 1. Locked or placeholder credentials ("*", "x", "!", "!!", "*LK*", "*NP*", "!hash" lock prefix). In uh_auth_check the plaintext compare branch fires whenever realm->pass does not start with '$', so any client sending the placeholder verbatim as the password matched and authenticated. On OpenWrt every system account except root has '*' or 'x' in /etc/shadow by default, so a realm configured as '/cgi-bin/admin:adminuser:$p$daemon' (the pattern the example UCI in package/network/services/uhttpd/files/uhttpd.config documents) authenticated any request with password '*' and ran the CGI as root. 2. Empty password (the default OpenWrt root entry, "root:::..."): handled correctly by the existing empty-pass drop but silently, leaving admins unaware that '$p$root' on a fresh image produces a public URL. Match login(1) semantics: locked or placeholder accounts deny all access, accounts with no password set permit access without authentication, accounts with a real hash require it. For each non-hash case log a distinct warning so admins notice the silent state. A '$p$' reference to a non-existent account is treated as a config typo: deny all access (consistent with the admin's stated intent of requiring auth) and log a warning. For the locked case, bind the realm to a sentinel that starts with '$' (skipping the plaintext compare branch) and cannot be produced by crypt(3) (failing the hash branch), so every request returns 401 instead of falling through to a missing-realm public response. Reported-by: Amit Pinchasi <amitpinchasi123@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Link: https://github.com/openwrt/uhttpd/pull/25 Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
-rw-r--r--auth.c38
1 files changed, 36 insertions, 2 deletions
diff --git a/auth.c b/auth.c
index 46b7bcb..45cb09a 100644
--- a/auth.c
+++ b/auth.c
@@ -72,10 +72,44 @@ void uh_auth_add(const char *path, const char *user, const char *pass)
#endif
if (!new_pass) {
pwd = getpwnam(&pass[3]);
- if (pwd && pwd->pw_passwd && pwd->pw_passwd[0] &&
- pwd->pw_passwd[0] != '!')
+ if (pwd && pwd->pw_passwd && pwd->pw_passwd[0])
new_pass = pwd->pw_passwd;
}
+
+ /* Only honour a real crypt(3) hash. Anything else from shadow
+ * splits into two cases that mirror login(1) semantics:
+ *
+ * - Empty password field ("root:::..."): the system account
+ * permits passwordless login, so leave new_pass empty and
+ * let the realm be dropped at the next check, which leaves
+ * the protected path unauthenticated.
+ *
+ * - Any other non-'$' value ("*", "x", "!", "!!", "*LK*",
+ * "*NP*", "!hash" lock prefix, ...): the account is locked
+ * or its credential is a placeholder. Without this guard
+ * uh_auth_check's plaintext compare branch would fire (it
+ * runs whenever realm->pass does not start with '$') and a
+ * client could authenticate by sending the placeholder
+ * verbatim as the password. Bind the realm to a sentinel
+ * that starts with '$' (skipping the plaintext branch) and
+ * that crypt(3) cannot produce (failing the hash branch),
+ * so every request gets 401 instead of silent public
+ * access. */
+ if (!new_pass) {
+ fprintf(stderr, "uhttpd: account '%s' does not "
+ "exist; denying all access to %s\n",
+ &pass[3], path);
+ new_pass = "$";
+ } else if (new_pass[0] && new_pass[0] != '$') {
+ fprintf(stderr, "uhttpd: account '%s' is locked "
+ "or has a non-crypt password; denying all "
+ "access to %s\n", &pass[3], path);
+ new_pass = "$";
+ } else if (!new_pass[0]) {
+ fprintf(stderr, "uhttpd: account '%s' has no "
+ "password set; leaving %s unauthenticated\n",
+ &pass[3], path);
+ }
} else {
new_pass = pass;
}