#!/bin/sh # Regression tests from the audits of September and October 2026. Each case # is a config, repository or request that used to crash cgit, end it inside # git, or hand a visitor something other than what was asked for. test_description='Check the audit regressions' . ./setup.sh test_expect_success 'set up a repository and a config to vary' ' mkrepo repos/rob 3 && sha=$(git -C repos/rob rev-parse HEAD~1) && { echo "virtual-root=/" && echo "cache-size=0" && echo "snapshots=tar.gz" && echo "repo.url=rob" && echo "repo.path=$PWD/repos/rob/.git" } >robrc ' robq() { CGIT_CONFIG="$PWD/robrc" QUERY_STRING="$1" cgit; } test_expect_success 'a repository without a path answers 404' ' { echo "virtual-root=/" && echo "cache-size=0" && echo "repo.url=nopath" && echo "repo.url=emptypath" && echo "repo.path=" } >nopathrc && for r in nopath emptypath do CGIT_CONFIG="$PWD/nopathrc" QUERY_STRING="url=$r/log/" cgit >tmp && grep "^Status: 404 Not Found" tmp && grep "Failed to open $r: Not a valid git repository" tmp || return 1 done ' test_expect_success 'a repository with an empty url is skipped with a warning' ' { echo "virtual-root=/" && echo "cache-size=0" && echo "repo.url=" && echo "repo.path=$PWD/repos/rob/.git" && echo "repo.url=rob" && echo "repo.path=$PWD/repos/rob/.git" } >nourlrc && CGIT_CONFIG="$PWD/nourlrc" QUERY_STRING="url=rob/" cgit >tmp 2>err && grep "^Status: 200" tmp && grep "Ignoring repository with an empty url" err ' test_expect_success 'a scan path that is itself a repository is named after it' ' { echo "virtual-root=/" && echo "cache-size=0" && echo "remove-suffix=1" && echo "scan-path=$PWD/repos/rob" } >rootrc && CGIT_CONFIG="$PWD/rootrc" QUERY_STRING="url=" cgit >tmp && grep "toplevel-repo.>rob" tmp ' test_expect_success 'a repo key after scan-path is reported instead of applied' ' { echo "virtual-root=/" && echo "cache-size=0" && echo "repo.url=first" && echo "repo.path=$PWD/repos/rob/.git" && echo "scan-path=$PWD/repos" && echo "repo.desc=misplaced" } >stalerc && CGIT_CONFIG="$PWD/stalerc" QUERY_STRING="url=" cgit >tmp 2>err && ! grep "misplaced" tmp && grep "Ignoring repo.desc before any repo.url" err ' test_expect_success 'a valueless or broken git config in a scanned repository is skipped' ' git clone -q --bare repos/rob/.git scan/a.git && printf "[cgit]\n\thide\n[cgit\n" >>scan/a.git/config && { echo "virtual-root=/" && echo "cache-size=0" && echo "enable-git-config=1" && echo "scan-path=$PWD/scan" } >gitcfgrc && CGIT_CONFIG="$PWD/gitcfgrc" QUERY_STRING="url=" cgit >tmp 2>err && grep "toplevel-repo.>" tmp && grep "Ignoring unreadable config in $PWD/scan/a.git/config" err ' test_expect_success 'only descriptive keys are taken from a scanned repository' ' git clone -q --bare repos/rob/.git scan/b.git && { echo "desc=from the repo" && echo "readme=master:file-1" && echo "head-content=" && echo "logo-link=javascript:alert(2)" } >scan/b.git/cgitrc && CGIT_CONFIG="$PWD/gitcfgrc" QUERY_STRING="url=b.git/about/" cgit >tmp 2>err && grep "from the repo" tmp && grep "pre class=.plaintext." tmp && ! grep "alert(" tmp && grep "Ignoring head-content in $PWD/scan/b.git/: trust-scan-config is not set" err && grep "Ignoring logo-link in " err ' test_expect_success 'a filesystem readme from a scanned repository is refused' ' echo "readme=/etc/hosts" >scan/b.git/cgitrc && CGIT_CONFIG="$PWD/gitcfgrc" QUERY_STRING="url=b.git/about/" cgit >tmp 2>err && grep "Ignoring readme in " err ' # A repository served as html runs its pages on the site's own origin, so # the switch waits on trust like the keys that place markup. The warning has # to name the repository, which it did not for a key read from git config. test_expect_success 'html serving from a scanned repository waits on trust' ' ( cd repos/rob && printf "

page

\n" >page.html && git add page.html && git commit -m html ) && mkdir -p scan2 && git clone -q --bare repos/rob/.git scan2/c.git && git -C scan2/c.git config cgit.enable-html-serving 1 && { echo "virtual-root=/" && echo "cache-size=0" && echo "enable-git-config=1" && echo "mimetype.html=text/html" && echo "scan-path=$PWD/scan2" } >htmlrc && CGIT_CONFIG="$PWD/htmlrc" QUERY_STRING="url=c.git/plain/page.html" cgit >tmp 2>err && grep "^Content-Type: text/plain" tmp && grep "^X-Content-Type-Options: nosniff" tmp && grep "Ignoring enable-html-serving in $PWD/scan2/c.git/: trust-scan-config is not set" err ' test_expect_success 'with trust-scan-config the same repository serves html' ' { echo "trust-scan-config=1" && cat htmlrc } >htmltrustrc && CGIT_CONFIG="$PWD/htmltrustrc" QUERY_STRING="url=c.git/plain/page.html" cgit >tmp && grep "^Content-Type: text/html" tmp && ! grep "^X-Content-Type-Options" tmp ' test_expect_success SYMLINKS 'a symlink cycle under the scan path is entered once' ' ln -s . scan/loop && CGIT_CONFIG="$PWD/gitcfgrc" QUERY_STRING="url=" cgit >tmp && test $(grep -c "toplevel-repo" tmp) = 2 ' test_expect_success 'a bare query parameter does not swallow the next one' ' robq "url=rob/commit/&x&id=$sha" >tmp && grep "
commit 2" tmp ' test_expect_success 'a filter that cannot run answers one error page' ' { echo "commit-filter=exec:$PWD/no/such/filter" && cat robrc } >badfilterrc && CGIT_CONFIG="$PWD/badfilterrc" QUERY_STRING="url=rob/commit/" cgit >tmp 2>err && test $(grep -c "^Status:" tmp) = 1 && grep "Unable to complete the request" tmp && grep "Unable to run filter $PWD/no/such/filter" err && grep "Unable to exec filter" err ' test_expect_success 'a filter that exits without reading does not end cgit' ' { echo "email-filter=exec:/usr/bin/true" && cat robrc } >truerc && CGIT_CONFIG="$PWD/truerc" QUERY_STRING="url=rob/log/" cgit >tmp && grep "^Status:" tmp ' test_expect_success 'a commit encoding header is read without its newline' ' ( cd repos/rob && echo more >file-1 && git add file-1 && git -c i18n.commitEncoding=ISO-8859-1 commit -m "$(printf "caf\351")" ) && robq "url=rob/commit/" >tmp && grep "
caf$(printf "\303\251")<" tmp ' test_expect_success 'a submodule below the about path does not end the request' ' ( cd repos/rob && git update-index --add --cacheinfo 160000,$sha,sub && git commit -m gitlink ) && { echo "mimetype.png=image/png" && cat robrc && echo "repo.readme=master:file-1" } >aboutrc && CGIT_CONFIG="$PWD/aboutrc" QUERY_STRING="url=rob/about/sub" cgit >tmp 2>err && grep "^Status:" tmp && ! grep "fatal" err ' test_expect_success 'a missing image on the about page errors as html' ' CGIT_CONFIG="$PWD/aboutrc" QUERY_STRING="url=rob/about/missing.png" cgit >tmp && grep "^Status: 404" tmp && grep "^Content-Type: text/html" tmp ' test_expect_success 'a snapshot name holding a slash is refused' ' robq "url=rob/snapshot/:/../../...tar.gz" >tmp && grep "^Status: 404" tmp && robq "url=rob/snapshot/rob-master.tar.gz" >tmp && grep "^Status: 200" tmp ' test_expect_success 'a path opening with a colon is refused on the log and patch pages' ' robq "url=rob/log/:%25x" >tmp && grep "^Status: 400" tmp && robq "url=rob/patch/:%25x" >tmp && grep "^Status: 400" tmp && test $(grep -c "^Status:" tmp) = 1 ' test_expect_success 'a revision spelled like an option is refused' ' git -C repos/rob update-ref refs/heads/--stdin HEAD && robq "url=rob/log/&h=--stdin" >tmp && grep "^Status: 404" tmp && robq "url=rob/blame/file-1&id=--stdin" >tmp && grep "^Status: 400" tmp && robq "url=rob/atom/&h=--stdin" >tmp && grep "^Status: 404" tmp ' test_expect_success 'the blob page reads a file through an annotated tag' ' git -C repos/rob -c tag.gpgsign=false tag -a -m note ann HEAD && robq "url=rob/blob/&h=ann&path=file-1" >tmp && strip_headers body && printf "more\n" >want && test_cmp want body ' # The plain page walks a path out of whatever commit the id names, and the # blob page used to answer such a request with the raw commit object. test_expect_success 'the blob page walks a path out of the commit or tag the id names' ' tip=$(git -C repos/rob rev-parse HEAD) && robq "url=rob/blob/file-1&id=$tip" >tmp && strip_headers body && test_cmp want body && tag=$(git -C repos/rob rev-parse ann) && robq "url=rob/blob/file-1&id=$tag" >tmp && strip_headers body && test_cmp want body && robq "url=rob/blob/missing&id=$tip" >tmp && grep "^Status: 404" tmp ' test_expect_success 'the commit page shows a message whole when text follows its trailers' ' ( cd repos/rob && echo again >file-1 && git add file-1 && git commit -F - <<-\EOF Subject Body text. Signed-off-by: A U Thor Conflicts: file-1 EOF ) && { echo "enable-commit-trailers=1" && cat robrc } >trailrc && CGIT_CONFIG="$PWD/trailrc" QUERY_STRING="url=rob/commit/" cgit >tmp && ! grep "commit-trailers" tmp && grep "Conflicts:" tmp ' test_expect_success 'a revision expression that walks the history is refused' ' robq "url=rob/log/&h=:/zzzz" >tmp && grep "^Status: 404" tmp && robq "url=rob/commit/&id=HEAD^{/zzzz}" >tmp && grep "^Status: 400" tmp && robq "url=rob/log/&qt=range&q=:/zzzz" >tmp && grep "^Status: 400" tmp && robq "url=rob/log/&qt=range&q=master~1..master" >tmp && grep "^Status: 400" tmp && robq "url=rob/log/&qt=range&q=$sha..master" >tmp && grep "^Status: 200" tmp ' test_expect_success 'the log search is literal' ' robq "url=rob/log/&qt=grep&q=commit.1" >tmp && ! grep ">commit 1" tmp && robq "url=rob/log/&qt=grep&q=commit%201" >tmp && grep ">commit 1" tmp ' test_expect_success 'a commit with a broken tree line does not crash the blob page' ' git clone -q repos/rob broken && ( cd broken && bad=$(git cat-file commit HEAD | sed "s/^tree .*/tree not-a-hash/" | git hash-object -t commit -w --stdin --literally) && echo $bad >.git/refs/heads/bad ) && { echo "virtual-root=/" && echo "cache-size=0" && echo "repo.url=broken" && echo "repo.path=$PWD/broken/.git" && echo "repo.readme=:file-1" } >brokenrc && CGIT_CONFIG="$PWD/brokenrc" QUERY_STRING="url=broken/blob/&h=bad&path=file-1" cgit >tmp && grep "^Status: 404" tmp ' test_expect_success 'a die inside git answers one 500 page and logs the reason' ' printf "[core\n" >>broken/.git/config && CGIT_CONFIG="$PWD/brokenrc" QUERY_STRING="url=broken/log/" cgit >tmp 2>err && grep "^Status: 500" tmp && test $(grep -c "^Status:" tmp) = 1 && grep "bad config line" err && ! grep "bad config line" tmp ' test_expect_success 'a history with a missing parent renders up to the gap' ' git clone -q repos/rob gap && older=$(git -C gap rev-parse HEAD~3) && parent=$(git -C gap rev-parse HEAD~2) && rm gap/.git/objects/$(echo $parent | cut -c1-2)/$(echo $parent | cut -c3-) && { echo "virtual-root=/" && echo "cache-size=0" && echo "repo.url=gap" && echo "repo.path=$PWD/gap/.git" } >gaprc && CGIT_CONFIG="$PWD/gaprc" QUERY_STRING="url=gap/log/" cgit >tmp 2>err && test $(grep -c "^Status:" tmp) = 1 && grep "Unable to complete the request" tmp && grep "Failed to traverse parents" err && CGIT_CONFIG="$PWD/gaprc" QUERY_STRING="url=gap/patch/&id2=$older" cgit >tmp && test $(grep -c "^Status:" tmp) = 1 && grep "^Status: 500" tmp ' test_expect_success 'a filter pipeline sees the default SIGPIPE disposition' ' cat >pipe.sh <<-\EOF && #!/bin/sh n=0 while test $n -lt 20000 do echo line n=$((n + 1)) done | head -c 5 EOF chmod +x pipe.sh && { echo "source-filter=exec:$PWD/pipe.sh" && cat robrc } >piperc && CGIT_CONFIG="$PWD/piperc" QUERY_STRING="url=rob/tree/file-1" cgit >tmp 2>err && grep "^Status: 200" tmp && ! grep -i "broken pipe" err ' test_expect_success 'the fallback branch skips a name spelled like an option' ' git -C repos/rob update-ref refs/heads/-first HEAD && git -C repos/rob symbolic-ref HEAD refs/heads/gone && robq "url=rob/" >tmp && git -C repos/rob symbolic-ref HEAD refs/heads/master && grep "^Status: 200" tmp ' # Such a branch is refused as a head, so the switcher must not offer a # choice that can only lead to an error page. test_expect_success 'the branch switcher leaves out a name spelled like an option' ' grep "