package zitcli
import (
"errors"
"testing"
)
// A URL segment must not become a zit option.
//
// THE VULNERABILITY: the file, tree, doc and history routes put a
// request's path and version segments into argv positionally, and zit
// parses anything starting with "-" as an option with no "--" marker to
// stop it. So, anonymously, on every repository, needing no capability:
//
// GET /api/v1/r/<repo>/file/trunk/--help
// -> 200, and the file's "content" is zit's help text
//
// Found by jva's adversarial subagent, which re-read a ticket filed as a
// cosmetic 500 and asked what was on the other end of the 500.
func TestAUrlSegmentCannotBecomeAnOption(t *testing.T) {
for _, bad := range []string{
"--help", // the proof-of-concept: returned 200 with zit's help
"-trunk", // seen in a leaked command line from the tree route
"--no-cache=1", // a real flag with an argument attached
"-R", // NOT in this list by accident: see the next test
} {
if bad == "-R" {
continue // quarry's own; covered below
}
err := checkArgs([]string{"cat", "-R", "/srv/repo", bad, "trunk"})
if err == nil {
t.Errorf("FAIL: %q would have been passed to zit as an option", bad)
continue
}
if !errors.Is(err, ErrUnsafeArgument) {
t.Errorf("FAIL: %q refused with the wrong error: %v", bad, err)
}
t.Logf("ok: %q refused", bad)
}
}
// Quarry's OWN flags still pass, or the forge stops working. This is
// the half that makes the guard usable rather than a blanket ban, and
// the full suite is the real check on it.
func TestQuarrysOwnFlagsStillPass(t *testing.T) {
for _, args := range [][]string{
{"cat", "-R", "/srv/repo", "README.md", "trunk"},
{"timeline", "-R", "/srv/repo", "-n", "40"},
{"diff", "-R", "/srv/repo", "--from", "a", "--to", "b"},
{"commit", "-R", "/srv/repo", "-m", "msg", "--user", "dan"},
{"tag", "add", "--raw", "--propagate", "closed", "abc"},
{"ls", "-R", "/srv/repo", "--long"},
} {
if err := checkArgs(args); err != nil {
t.Errorf("FAIL: a legitimate invocation was refused: %v (%v)", err, args)
continue
}
t.Logf("ok: %v", args[:2])
}
}
// A negative number is an operand. Refusing it would break callers for
// no security gain, since no zit option is a bare number.
func TestANegativeNumberIsAnOperand(t *testing.T) {
if err := checkArgs([]string{"timeline", "-R", "/srv/repo", "-n", "-20"}); err != nil {
t.Fatalf("FAIL: a negative operand was refused: %v", err)
}
// But something that merely STARTS like one is not.
if err := checkArgs([]string{"cat", "-12abc"}); err == nil {
t.Fatal("FAIL: -12abc was accepted; only digits make a number")
}
t.Log("ok: -20 passes, -12abc does not")
}
// isArtifactName is what stops a verb travelling as a hash (G-44): zit
// prints a sentence for accept, and its first word is "accepted".
func TestAVerbIsNotAnArtifactName(t *testing.T) {
for s, want := range map[string]bool{
"accepted": false,
"rejected": false,
"": false,
"82d39746": false, // an abbreviation is not the name
"82d397468042fc0b83a709c0194e5befebd6026472b5bf0b955845af68643efb": true,
"5e2cbbb1b2bea27df01b2ba5c6882104aad3c601": true,
"82D397468042FC0B83A709C0194E5BEFEBD6026472B5BF0B955845AF68643EFB": false,
} {
if got := isArtifactName(s); got != want {
t.Errorf("isArtifactName(%q) = %v, want %v", s, got, want)
}
}
}