Skip to content
quarry Artifact [1e90f39796]
Login

Artifact [1e90f39796]

Artifact 1e90f397962634209bdc2a885564cc1074658d0baf6ba9fb7e116885a3353648:


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)
		}
	}
}