diff --git a/packages/mosaic/framework/tools/git/test-wrapper-guard.sh b/packages/mosaic/framework/tools/git/test-wrapper-guard.sh index 68e24467..a280e41a 100755 --- a/packages/mosaic/framework/tools/git/test-wrapper-guard.sh +++ b/packages/mosaic/framework/tools/git/test-wrapper-guard.sh @@ -314,6 +314,29 @@ FIXTURES="$TMP/fixtures.tsv" printf '0\t{"tool_input":{"command":"mygh api -X POST repos/a/b/issues -f title=x"}}\tmygh is not gh, and the scope gate must not over-admit either\n' printf '0\t{"tool_input":{"command":"/usr/bin/gh api repos/a/b/issues"}}\ta read through an absolute path is still a read\n' + # Quotes and backslashes INSIDE the word. The previous repair replaced quote + # characters with whitespace, which is token separation and not quote removal: + # a shell removes a quote without splitting the word around it, so `cu"rl"` is + # one word naming curl while whitespace made it two words naming neither. + # `"/usr/bin/curl"` passed under that version only because the inserted space + # happened to land after a slash, which established nothing. + printf '2\t{"tool_input":{"command":"cu\\"rl\\" --config /tmp/provider-write.cfg"}}\ta quote inside the word does not make it another program\t--config/-K\n' + printf '2\t{"tool_input":{"command":"cu'"'"'rl'"'"' --config /tmp/provider-write.cfg"}}\tand a single quote inside it is the same word again\t--config/-K\n' + printf '2\t{"tool_input":{"command":"/usr/bin/cu\\\\rl --config /tmp/provider-write.cfg"}}\tescaping is ordinary word formation, not a disguise\t--config/-K\n' + printf '2\t{"tool_input":{"command":"g\\"h\\" api -X POST repos/a/b/issues -f title=x"}}\tthe CLI name is a word on the same terms\n' + printf '2\t{"tool_input":{"command":"/usr/bin/g\\\\h api -X POST repos/a/b/issues -f title=x"}}\tincluding when it is escaped behind a path\n' + # The FLAG is the same recognition problem as the name, and is read the same + # way. No review raised this one; the name was simply the easier half to reach. + printf '2\t{"tool_input":{"command":"curl --con\\"fig\\" /tmp/provider-write.cfg"}}\tone word spelling --config is still --config\t--config/-K\n' + + # The unreadable-endpoint arm is the THIRD name consumer. It kept a private + # bare-name copy of the scope gate's regex, so a caller could be admitted by + # the repaired gate and then go unrecognized by the fail-closed refinement — + # a gate and its own refinement disagreeing about who the caller is. + printf '2\t{"tool_input":{"command":"/usr/bin/gh api -X POST repos/a/b/$EP -f title=x"}}\ta path-qualified CLI with an assembled endpoint is still unreadable\n' + printf '2\t{"tool_input":{"command":"g\\"h\\" api -X POST repos/a/b/$EP -f title=x"}}\tand so is a quoted one, which is where the two halves disagreed\n' + printf '0\t{"tool_input":{"command":"mygh api -X POST repos/a/b/$EP -f title=x"}}\tmygh is still not gh, in the refinement as well as the gate\n' + # Percent-encoded endpoints. Not hypothetical: /issues/1174 and /iss%%75es/1174 # both returned HTTP 200 with the same object from the live forge, so the # encoded spelling IS the wrapped endpoint and the literal comparison below it diff --git a/packages/mosaic/framework/tools/git/wrapper-guard.sh b/packages/mosaic/framework/tools/git/wrapper-guard.sh index bd826a94..963d4ead 100755 --- a/packages/mosaic/framework/tools/git/wrapper-guard.sh +++ b/packages/mosaic/framework/tools/git/wrapper-guard.sh @@ -48,25 +48,35 @@ CMD="$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null | # joined form, because that is the command. CMD="$(printf '%s' "$CMD" | sed -e ':a' -e 'N' -e '$!ba' -e 's/\\\n//g')" -# A second reading of the SAME command, used only by the two checks below that -# have to recognize a program by name. Quote characters and the punctuation of -# command substitution become whitespace, so a name that the shell will resolve -# is a word here regardless of how it was dressed: +# A second reading of the SAME command, used by every check below that has to +# recognize a program by NAME. Quote characters, backslashes and the punctuation +# of command substitution are DELETED, so a name the shell will resolve is a word +# here regardless of how it was dressed: # # "/usr/bin/curl" --config /tmp/req quoted absolute path # './curl' --config /tmp/req quoted relative path # $(which curl) --config /tmp/req command substitution # `which curl` --config /tmp/req the older spelling of the same thing # /usr/bin/gh api -X POST repos/a/b/... absolute path to a provider CLI +# cu"rl" --config /tmp/req quotes INSIDE the word +# /usr/bin/cu\rl --config /tmp/req a backslash inside the word +# g"h" api -X POST repos/a/b/... the same, in the provider-CLI name # -# All five executed the real program and all five returned ALLOW from the two -# name checks, at this file's previous head and the one before it. The first -# repair of this class matched curl as a basename and stopped there, which fixed -# the unquoted spelling only: the check still modelled ONE TEXTUAL PRESENTATION -# of a shell word rather than the word itself, so adding two quote characters -# restored the bypass, and the same defect sat untouched in the provider-CLI arm -# the whole time. Recognizing a name is either done after quote removal or it is -# caller-name parsing wearing a longer regex. +# Every one of them executed the real program and every one returned ALLOW, at +# each of this file's three previous heads. The repairs went bare word, then +# unquoted basename, then quotes-as-separators; each fixed a PRESENTATION and +# left the class, and the third is worth spelling out because it is the subtlest +# and it was mine: replacing quote characters with whitespace is token +# SEPARATION, not quote removal. A shell removes a quote WITHOUT splitting the +# word around it, so `cu"rl"` is one word naming curl, while whitespace made it +# two words naming neither. `"/usr/bin/curl"` blocked under that version only +# because the whitespace happened to land after a slash — a passing case that +# established nothing. +# +# So the characters are DELETED rather than replaced, which is what quote removal +# is, and backslashes go with them because escaping is ordinary word formation +# too. Deletion also handles substitution: `$(which curl)` becomes `which curl`, +# where the name is a word on its own. # # This does NOT try to be a shell. It cannot see a name that is absent from the # text — assembled from variables, or reached through a wrapper script that @@ -79,8 +89,7 @@ CMD="$(printf '%s' "$CMD" | sed -e ':a' -e 'N' -e '$!ba' -e 's/\\\n//g')" # these commands inside quotes on a Bash line is refused too. Applying that same # rule here keeps the file coherent — the alternative is a guard where the # payload arm treats quotes as text and the name arms treat them as armour. -CMD_NAMES="$(printf '%s' "$CMD" | tr '"'"'"'\`()' ' ')" -CMD_NAMES="$(printf '%s' "$CMD_NAMES" | sed 's/\$/ /g')" +CMD_NAMES="$(printf '%s' "$CMD" | tr -d '"'"'"'\`()$\\')" # The one place the shape of a program NAME is written down. Both callers below # use it, so the next fix to this class lands in a single location instead of @@ -340,8 +349,12 @@ fi # command text at all. That is a limit of inspecting a command string rather # than a defect this pattern can close, and it is stated rather than left for # the next reader to find. +# The FLAG is read from $CMD_NAMES too. It is the same recognition problem as +# the name — `--con"fig"` is one word spelling --config — and no review has +# raised it yet only because the name was the easier half to reach. Reading both +# halves the same way is the point of having one normalization. if printf '%s' "$CMD_NAMES" | grep -Eq "${NAME_PREFIX}curl([[:space:]]|$)" \ - && printf '%s' "$CMD" | grep -Eq -- '(^|[[:space:]])(-[A-Za-z]*K([[:space:]=]|$|[^[:space:]])|--config([[:space:]=]|$))'; then + && printf '%s' "$CMD_NAMES" | grep -Eq -- '(^|[[:space:]])(-[A-Za-z]*K([[:space:]=]|$|[^[:space:]])|--config([[:space:]=]|$))'; then cat <