cubeegress: validate match field types, and make rule_matches fail closed - #1459
cubeegress: validate match field types, and make rule_matches fail closed#1459dwin-gharibi wants to merge 1 commit into
Conversation
Signed-off-by: Dwin Gharibi <dwin.gharibi@email.kntu.ac.ir>
| if type(r.match) ~= "table" then | ||
| return false, "rules[" .. i .. "].match required (object; empty {} allowed)" | ||
| end | ||
| for _, field in ipairs({"sni", "host", "path", "scheme"}) do |
There was a problem hiding this comment.
Partially redundant with the existing checks in validate_match_tuples (called a few lines below this loop): a non-string scheme is already rejected by port_scheme.normalize_scheme/expand with the same message (rules[i].match.scheme must be a string), and non-string host/sni are already rejected by normalize_identity (rules[i].match host/sni ... is invalid: empty). So the genuinely new install-time rejections here are method (below) and path. Harmless defense-in-depth — but the PR's install-time table claims master "accepted" scheme={}, scheme=80, host={}, sni=12345; those were already 400s on master. The actual install-time behavior change is limited to malformed method/path.
| if r.match.method ~= nil then | ||
| if type(r.match.method) ~= "table" then | ||
| return false, string.format("rules[%d].match.method must be an array of strings", i) | ||
| end |
There was a problem hiding this comment.
The check only verifies type == "table", so an object-shaped table (e.g. {"GET" = true}) or a holey array ({1="GET", 3="POST"}) passes install validation — ipairs iterates nothing, or stops at the first nil gap, so index 3 is never validated. In rule_matches, ipairs then yields nothing or stops early, so the rule silently never matches — a dead rule, despite the "must be an array of strings" message. cjson-decoded JSON arrays are always contiguous, so this is only reachable via hand-built tables and is fail-closed either way; verifying it's a real array (e.g. #method equals the count of integer keys) would make the error message true.
| local hit = false | ||
| for _, mm in ipairs(m.method) do | ||
| if string.upper(mm) == ctx.method then hit = true; break end | ||
| if type(mm) == "string" and string.upper(mm) == ctx.method then hit = true; break end |
There was a problem hiding this comment.
Minor asymmetry with the scheme guard at line 145: a non-string method entry is skipped, so a policy that reached the dict without validation with method = [1, "GET"] would still match GET, whereas the scheme guard fails the whole rule on any non-string. If the stated intent is strictly fail-closed ("the data plane should deny rather than 500"), returning false on the first non-string entry would be consistent with the scheme behavior; as written, only the all-garbage case flips from 500 → deny. No effect on valid all-string policies — purely a question of intent on malformed ones.
Review: cubeegress match field type validation + fail-closed rule_matchesOverall: looks correct and well-scoped — approved with minor notes. All inline findings are low-severity/informational; no correctness or security defects found in the changed code. What this doesTwo layers of hardening for malformed policy
Verified sound
Findings (all low severity)
Suggestions
This review was AI-generated; no human approval is implied. |
chenhengqi
left a comment
There was a problem hiding this comment.
I personally don't like these runtime type checking. We can't hit these paths from API/SDK side.
Closes #1458.
Motivation
validate_policyverified thatrule.matchis a table and stopped there.rule_matchesthen calledstring.upperon eachmatch.methodentry andstring.loweronmatch.scheme, so a policy the admin APIhad accepted produced an uncaught Lua error inside
access_by_lua_block— nginx 500 for every request fromthat sandbox, with nothing pointing at the policy as the cause.
What this changes
Two layers, because either alone leaves a gap.
1.
CubeEgress/lua/policy.lua— reject malformed policies at install time.validate_policynowrequires
match.sni/match.host/match.path/match.schemeto be strings when present, andmatch.methodto be an array of strings. Errors name the rule index and the field, matching the existingmessage style. An empty
match = {}is still allowed, as documented.2.
CubeEgress/lua/access_phase.lua— makerule_matchesfail closed. A non-stringmethodentry isskipped rather than passed to
string.upper, and a non-stringschemereturns no-match rather than beingpassed to
string.lower. This matters because a policy can reach the shared dict without going throughvalidate_policy(bootstrap load, or anything written directly), and the data plane should deny rather than500 in that case.
No comment changes.
Testing
Install-time validation — all 11 cases behave as intended, where master accepted 8 of them:
with messages like
rules[1].match.method[1] must be a string.Request-path hardening — driving the real
rule_matchesvia its exported_rule_matcheshook:Matching semantics unchanged — the same harness checks the normal cases, and the credential-injection
harness from the HTTP-inject issue is re-run as a regression check:
Case 3 is the separate plaintext-injection issue, tracked on its own branch — unchanged here, and shown so it
is clear this PR did not alter it.
CI gates: the Lua files are not covered by
fmt-check(no Lua formatter configured) or byunit-test-check. Verification is via LuaJIT against the real modules, as above.Risk / rollout
A
PUTthat previously returned200for a malformed rule now returns400. Any control-plane componentpushing such a rule would start seeing failures — but those rules were producing 500s on the data plane
anyway, so surfacing the error at install time is strictly better.