From 6b0cd465a04328f78ededd02a08062a519611193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Gro=C3=9Fmann?= Date: Sun, 9 Aug 2026 23:42:54 +0200 Subject: [PATCH] Treat a malformed route line carrying a policy as a config error A LOC=/MOD= line whose positional token is missing and whose first token is an AUTH=/RES= option was dropped with a warning, and the server started: LOC=AUTH=BASIC RES=FACILITY:HTTPD.ADMIN published exactly the subtree it named, because an unregistered route is served under the global LOGIN default. Same fail-open as #161 but reachable from a typo, no allocation failure needed. MOD= was worse in a different direction: it never inspected tok[0], so the option was folded into an 8-character program name ("AUTH=BAS") and registered against a derived "*.auth=bas" pattern -- a route built out of a typo that can never load. An empty MOD= said nothing at all. Both now reject the line and classify what it carried through the machinery #161 added: the remaining tokens are parsed only to decide whether the policy binds, so a binding one raises HTTPD419E and stops the server, while AUTH=NONE or no policy stays a warning. Nothing is registered either way, so the RES= storage the classification parse allocated is released again. Fixes #164 --- docs/configuration.md | 38 ++++++++++++++++++++++++++----------- src/httpprm.c | 44 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 6737a9a..5a1a541 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -77,17 +77,33 @@ serves a file or dispatches a CGI: > for that route rather than locking it. `DEBUG 1` traces every such check, so > verify a new `RES=` route against the debug log before relying on it. -A route that carries an auth policy is registered or the server does not start. -If the policy cannot be built or the route cannot be added (both only happen when -the region runs out of storage while the Parmlib is read), the server issues -`HTTPD418E`/`HTTPD419E` naming the route, then `HTTPD420E` and terminates before -the listener is bound. Silently dropping the route would serve its requests under -the global `LOGIN` default instead — for a `LOC=` prefix under `LOGIN NONE` that -means handing out the whole protected subtree. A route that fails to register -with no policy at all, or with `AUTH=NONE` only, stays a warning and the server -continues — the fallback can only be stricter than what it asked for. The one -exception is a `MOD=`/`LOC=` line the parser could not even tokenize: whether it -carried a policy is then unknowable, so it is treated as if it did. +**A route that carries an auth policy is registered or the server does not +start.** Dropping it is not a safe fallback: the route does not disappear, its +requests are served under the global `LOGIN` default instead — and for a `LOC=` +prefix under `LOGIN NONE` that hands out the whole subtree it was protecting. So +whenever such a route cannot be built, the server issues `HTTPD418E`/`HTTPD419E` +naming it, then `HTTPD420E`, and terminates before the listener is bound. Three +things reach that path: + +- the policy itself could not be built, or the route could not be added (both + only when the region runs out of storage while the Parmlib is read) +- the line could not be tokenized at all — whether it carried a policy is then + unknowable, so it is assumed to have +- **the positional token is missing and an option stands in its place** — the + typo case, no allocation failure needed: + + ``` + LOC=AUTH=BASIC RES=FACILITY:HTTPD.ADMIN path forgotten + MOD=AUTH=BASIC /zosmf/* program name forgotten + ``` + + The first once published exactly the subtree it named; the second folded the + option into an 8-character module name (`AUTH=BAS`) and registered a route + that could never load. + +A route that fails to register with no policy at all, or with `AUTH=NONE` only, +stays a warning and the server continues — the fallback can only be stricter +than what it asked for. | Option | Values | Description | |--------|--------|-------------| diff --git a/src/httpprm.c b/src/httpprm.c index 0b40d88..dfecc87 100644 --- a/src/httpprm.c +++ b/src/httpprm.c @@ -557,6 +557,34 @@ route_policy_lost(HTTPD *httpd, const char *kind, const char *path) httpd->flag |= HTTPD_FLAG_CFGERR; } +/* route_malformed() - a MOD=/LOC= line was rejected before a route could be +** built, because the positional token it needs (program name / path) is missing +** and an AUTH=/RES= option stands in its place. A dropped line is not +** harmless: the prefix it was meant to gate is then served under the global +** LOGIN default, so `LOC=AUTH=BASIC RES=FACILITY:HTTPD.ADMIN` -- the path +** forgotten -- publishes exactly the subtree it named (issue #164). Unlike the +** allocation failures this shares its reporting with, a typo reaches it. +** +** The remaining tokens are parsed only to classify the line: a binding policy +** makes it a configuration error, AUTH=NONE or no policy stays a warning +** (see policy_binds()). Nothing is registered either way, so any RES= storage +** the parse allocated is released again. */ +static void +route_malformed(HTTPD *httpd, const char *kind, char **tok, int ntok, + const char *value) +{ + ROUTE_POLICY pol; + int binds; + + memset(&pol, 0, sizeof(pol)); /* auth = HTTP_AUTH_DEFAULT */ + parse_kv_tail(httpd, tok, 0, ntok, &pol); /* every token is an option */ + binds = policy_binds(&pol); + apply_policy(NULL, &pol); /* release RES= strings */ + + if (binds) + route_policy_lost(httpd, kind, value); +} + /* ==================================================================== ** Parse MOD=PROGRAM [pattern] [AUTH=mode] [RES=class:resource] ** If pattern is omitted, derive *. and use DOCROOT. @@ -590,6 +618,20 @@ parse_mod(HTTPD *httpd, const char *value) ntok = tokenize(tmp, tok, 8); if (ntok < 1) { /* no program name */ + wtof("HTTPD421W MOD= requires a program name " + "(e.g. MOD=MVSMF /zosmf/* AUTH=BASIC)"); + free(tmp); + return; + } + + /* An AUTH=/RES= option where the program name belongs means the name was + omitted. Without this the option would be folded into an 8-char module + name ("AUTH=BAS") and registered against a derived "*.auth=bas" pattern + -- a route that can never load, built out of a typo. */ + if (is_route_kv(tok[0])) { + wtof("HTTPD421W MOD= requires a program name " + "(e.g. MOD=MVSMF /zosmf/* AUTH=BASIC)"); + route_malformed(httpd, "MOD", tok, ntok, value); free(tmp); return; } @@ -674,6 +716,8 @@ parse_loc(HTTPD *httpd, const char *value) ntok = tokenize(tmp, tok, 8); if (ntok < 1 || is_route_kv(tok[0])) { /* first token must be a path */ wtof("HTTPD415W LOC= requires a path (e.g. LOC /admin/* AUTH=BASIC)"); + if (ntok >= 1) + route_malformed(httpd, "LOC", tok, ntok, value); free(tmp); return; }