Skip to content

fix(routing): polish CIDR/regex/port-range matchers and document capset stance#24

Open
andypost wants to merge 1 commit into
masterfrom
claude/audit-pr-i-routing-polish
Open

fix(routing): polish CIDR/regex/port-range matchers and document capset stance#24
andypost wants to merge 1 commit into
masterfrom
claude/audit-pr-i-routing-polish

Conversation

@andypost
Copy link
Copy Markdown
Owner

Summary

Audit-driven routing polish + isolation docs (PR-I from `security-audit.md` / PR #10). Six low-priority findings, all low-impact; closes the tail of the audit's actionable items.

After this lands, the only open audit findings are: PR-H (controller robustness — JSON depth/element caps, PHP TrueAsync EG state, Ruby per-request IO; not yet implemented), and two excluded by the maintainer's DoS policy.

Findings addressed

  • V2 [Medium] IPv4 `/32` CIDR fallthrough (`route_addr.c` — comment-only, documenting the implicit-EXACT-after-fallthrough intent)
  • V2 [Medium] Asymmetric pattern/request URI decoding (`route.c` — comment-only at `nxt_http_route_decode_str`; the two paths already share helpers)
  • V2 [Medium] `pcre2_match_data_create(0, ctx)` undefined (`route.c:2150` — bump size from 0 to 1, the documented minimum)
  • V2 [Low] Port-range parsing off-by-one (`route_addr.c` — validate `port.length >= 3` and drop the `- 1`)
  • V2 [Low] Host matching always lowercased (`route.c` — comment-only, citing RFC 9110 §4.2.3)
  • V6 [Informational] `capset()` never called (`nxt_capability.c` — top-of-file comment documenting that privilege barriers are setuid + PR_SET_NO_NEW_PRIVS, not cap drops)

Behaviour change

One: a route pattern with a malformed port range like `X-` (2 bytes, trailing dash) is now rejected with `NXT_ADDR_PATTERN_PORT_ERROR` via the single-port parse branch instead of being silently accepted. Pre-existing behaviour was to fall through and accept the host-only entry; no valid Unit config relies on this.

Files changed

4 files, +62 / -2.

Test plan

  • `./configure --openssl && make -j unitd` clean.
  • `pytest-3 test/test_routing.py test/test_routing_addr.py` requires root.

Upstream

Same fixes apply to `freeunitorg/freeunit`; will forward after merge here.


Generated by Claude Code

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request fixes a bug by ensuring a non-zero ovector size is passed to PCRE2 match-data creation and refines the handling of malformed port ranges in HTTP routing. Additionally, it introduces documentation clarifying capability detection, case-insensitive host matching, and URI decoding symmetry. Review feedback identifies an opportunity to simplify the port range validation to better reject malformed inputs and suggests explicitly setting the match type for /32 CIDR blocks to maintain consistency with the updated comments.

Comment thread src/nxt_http_route_addr.c Outdated
Comment on lines +297 to +301
if (port.length < 3) {
delim = NULL;
} else {
delim = memchr(port.start, '-', port.length);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The check port.length < 3 prevents the fix from working for 2-byte malformed inputs like 8-, which the pull request description specifically mentions as a target for rejection. By setting delim = NULL for 2-byte strings, these inputs continue to fall through to the single-port parsing branch (nxt_int_parse). If nxt_int_parse is lax, they will still be accepted. Removing this conditional and simply using memchr with port.length allows the range parsing logic to correctly identify and reject these malformed patterns because the second part of the range will be empty, causing the subsequent nxt_int_parse call to fail.

    delim = memchr(port.start, '-', port.length);

Comment thread src/nxt_http_route_addr.c
Comment on lines +265 to +269
/*
* /32 is the single-host case; fall through to the inet_addr
* parse below, but set EXACT here so the state machine reflects
* the user's intent rather than relying on the implicit default.
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment states that EXACT is set here, but the code actually falls through to line 277 where base->match_type is eventually assigned. To make the code match the documentation and improve clarity, it is better to explicitly set the match type within this block before exiting the CIDR parsing logic.

        /*
         * /32 is the single-host case; fall through to the inet_addr
         * parse below, but set EXACT here so the state machine reflects
         * the user's intent rather than relying on the implicit default.
         */
        base->match_type = NXT_HTTP_ROUTE_ADDR_EXACT;

…et stance

Audit-driven routing-polish + isolation-docs pass (PR-I from
security-audit.md / #10).  Six low-priority findings;
no behaviour change for any valid existing config.

* V2 [Medium] IPv4 /32 CIDR fallthrough (nxt_http_route_addr.c)
  When cidr_prefix == 32 the parser today falls through to the
  inet_addr path below, which then sets match_type = EXACT.  The
  current code is correct, but the fallthrough is fragile under
  refactor.  Add a comment making the intent explicit so a future
  reader doesn't add an early goto and accidentally skip the
  inet_addr parse.

* V2 [Medium] Asymmetric pattern/request URI decoding (nxt_http_route.c)
  Both configured pattern strings and request URIs decode through
  nxt_decode_uri / nxt_decode_uri_plus; %XX semantics are symmetric.
  Document the invariant at nxt_http_route_decode_str so a future
  helper change keeps the two paths in lock-step.

* V2 [Medium] nxt_regex_match_create(.., 0) size argument (route.c)
  pcre2_match_data_create(0, ctx) is undefined per the PCRE2 docs;
  1 is the documented minimum (one ovector pair, holding the
  overall match offsets — captures aren't consulted here).  Bump
  the size from 0 to 1 and document.

* V2 [Low] Port-range parsing off-by-one (route_addr.c)
  memchr(.., port.length - 1) silently accepted a trailing '-' in
  a 2-byte input ("N-").  Validate port.length >= 3 (the minimum
  for "N-N") before searching, drop the - 1, and let the
  single-port parse below reject the malformed range.

* V2 [Low] Host matching always lowercased (nxt_http_route.c)
  Host matching is hardcoded to LOWCASE; admin-supplied
  case-sensitivity is silently ignored.  Per RFC 9110 §4.2.3
  hosts are case-insensitive, so the behaviour is correct; just
  document it so the surprise is in the comment, not in the
  operator's logs.

* V6 [Informational] capset never called (nxt_capability.c)
  The module exposes capget for detection but does not call
  capset(2) to drop capabilities programmatically.  Operators
  expecting an explicit cap-drop step should not assume one
  exists — privilege barriers are setuid + PR_SET_NO_NEW_PRIVS.
  Top-of-file comment.

No behaviour change for any valid existing config; the only
visible change is that a route pattern with a malformed port
range like "X-" (2 bytes, trailing dash) is now rejected with
NXT_ADDR_PATTERN_PORT_ERROR via the single-port parse branch
instead of being silently accepted as a host-only entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@andypost andypost force-pushed the claude/audit-pr-i-routing-polish branch from 5dbeb17 to 352fae9 Compare May 11, 2026 21:32
@andypost
Copy link
Copy Markdown
Owner Author

Both Gemini findings addressed in 352fae97:

  • [high] Port-range "8-" rejection — dropped the port.length < 3 conditional and simplified to memchr(port.start, '-', port.length) as suggested. 8- now hits the range-parse branch, the empty second half fails nxt_int_parse, and the function returns NXT_ADDR_PATTERN_PORT_ERROR — no reliance on nxt_int_parse leniency.
  • [medium] /32 EXACT explicit — set base->match_type = NXT_HTTP_ROUTE_ADDR_EXACT inside the /32 block so the state machine matches the comment.

Force-pushed; build clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant