#!/bin/sh
# Collects the regression tests for the security fixes and for the behaviour
# this fork adds on top of upstream cgit. Each case builds the smallest
# repository and config that reproduce the original problem and then asks
# for the page that used to mishandle it.
test_description='Check security fixes and fork-specific behavior'
. ./setup.sh
# Most of what follows shares one repository and one config, so the fixture
# carries everything they need at once, a blob over the size limit, readmes
# holding markup that must not reach the page as markup, and a subdirectory
# to sort ahead of the files.
test_expect_success 'set up security fixtures' '
mkrepo repos/sec 1 &&
(
cd repos/sec &&
dd if=/dev/zero bs=1024 count=4 2>/dev/null | tr "\0" "X" >big.txt &&
printf "# Title\n\n" >README.md &&
printf "\n" >readme.txt &&
printf "top\n" >afile &&
mkdir zsub &&
printf "inner\n" >zsub/inner &&
git add -A &&
git commit -m fixtures
) &&
{
echo "virtual-root=/" &&
echo "cache-size=0" &&
echo "max-blob-size=1" &&
echo "enable-blame=1" &&
echo "enable-tree-group-dirs=1" &&
echo "repo.url=sec" &&
echo "repo.path=$PWD/repos/sec/.git"
} >seccgitrc
'
secq() { CGIT_CONFIG="$PWD/seccgitrc" QUERY_STRING="$1" cgit; }
# A revision beginning with a dash reaches git as an option rather than as a
# tip, so a request for id=--output= could create or truncate any file
# the server is able to write.
test_expect_success 'log id=--output does not write a file' '
rm -f pwned &&
cgit_query "url=foo/log&id=--output=$PWD/pwned" >tmp 2>&1 &&
! test -e pwned
'
test_expect_success 'log id=--output is rejected as an invalid revision' '
grep -i "invalid revision" tmp
'
test_expect_success 'a normal log still renders' '
cgit_query "url=foo/log" >tmp &&
grep -i "commit 5" tmp
'
test_expect_success 'a valid id= still renders the log' '
sha=$(git -C repos/foo rev-parse HEAD) &&
cgit_query "url=foo/log&id=$sha" >tmp &&
grep -i "commit 5" tmp
'
# max-blob-size is enforced before the object is read, so every view that
# would otherwise inline a file has to turn the same one away rather than
# inflate it first and think better of it afterwards.
test_expect_success 'tree view refuses an oversized blob' '
secq "url=sec/tree/big.txt" | grep -iE "exceeds|too large"
'
test_expect_success 'plain view refuses an oversized blob' '
secq "url=sec/plain/big.txt" | grep -iE "exceeds|too large|413"
'
test_expect_success 'blame view refuses an oversized blob' '
secq "url=sec/blame/big.txt" | grep -iE "exceeds|too large"
'
test_expect_success 'a small blob is still served' '
secq "url=sec/plain/afile" | grep -F "top"
'
# A readme is repository content, so with no about filter configured it has
# to reach the page escaped instead of as live markup, whatever its name
# suggests about the format.
test_expect_success 'markdown readme without a filter is escaped as plain text' '
{
echo "virtual-root=/" &&
echo "cache-size=0" &&
echo "repo.url=md" &&
echo "repo.path=$PWD/repos/sec/.git" &&
echo "repo.readme=master:README.md"
} >secmdrc &&
CGIT_CONFIG="$PWD/secmdrc" QUERY_STRING="url=md/about/" cgit >tmp &&
grep "pre class=.plaintext." tmp &&
grep "<script>" tmp &&
! grep "" tmp
'
test_expect_success 'non-markdown readme without a filter is escaped' '
{
echo "virtual-root=/" &&
echo "cache-size=0" &&
echo "repo.url=txt" &&
echo "repo.path=$PWD/repos/sec/.git" &&
echo "repo.readme=master:readme.txt"
} >sectxtrc &&
CGIT_CONFIG="$PWD/sectxtrc" QUERY_STRING="url=txt/about/" cgit >tmp &&
grep "<script>" tmp &&
! grep "" tmp
'
test_expect_success 'non-markdown readme keeps its line structure' '
grep "pre class=.plaintext." tmp
'
# A Content-Security-Policy without unsafe-inline stops an inline onchange
# handler from ever running, so the option form submits through a plain
# button and its selects carry no handlers at all.
test_expect_success 'diff options submit through a button' '
sha=$(git -C repos/foo rev-parse HEAD) &&
cgit_query "url=foo/commit&id=$sha" >tmp &&
grep "type=.submit. value=.reload." tmp &&
! grep "onchange" tmp
'
# Grouping directories ahead of files is behaviour this fork adds, so nothing
# upstream covers it.
test_expect_success 'tree groups directories before files' '
secq "url=sec/tree/" >tmp &&
dirline=$(grep -n "tree/zsub" tmp | head -1 | cut -d: -f1) &&
fileline=$(grep -n "tree/afile" tmp | head -1 | cut -d: -f1) &&
test -n "$dirline" &&
test -n "$fileline" &&
test "$dirline" -lt "$fileline"
'
# A file name is repository content and may hold a quote, which would break
# out of the href attribute on the line number links of a side by side diff,
# so the path is percent-encoded on its way into them.
test_expect_success 'ssdiff percent-encodes a quoted file path' '
mkrepo repos/xss 1 &&
name=$(printf "x\047y.txt") &&
(
cd repos/xss &&
printf "a\nb\n" >"$name" &&
git add -A &&
git commit -m add &&
printf "a\nc\n" >"$name" &&
git commit -am change
) &&
{
echo "virtual-root=/" &&
echo "cache-size=0" &&
echo "repo.url=xss" &&
echo "repo.path=$PWD/repos/xss/.git"
} >xssrc &&
sha=$(git -C repos/xss rev-parse HEAD) &&
CGIT_CONFIG="$PWD/xssrc" QUERY_STRING="url=xss/diff/&id=$sha&ss=1" cgit >tmp &&
grep "tree/x%27y.txt" tmp &&
! grep "href=.[^>]*x.y.txt.[^>]*>" tmp
'
# git itself will write a commit with an empty message, so the log and the
# summary both have to have something to print where the subject goes.
test_expect_success 'a message-less commit renders without crashing' '
mkrepo repos/nomsg 1 &&
(
cd repos/nomsg &&
tree=$(git write-tree) &&
printf "tree %s\nauthor a 1735689600 +0000\ncommitter a 1735689600 +0000\n" "$tree" >raw &&
cid=$(git hash-object -t commit -w raw) &&
git update-ref refs/heads/master "$cid"
) &&
{
echo "virtual-root=/" &&
echo "cache-size=0" &&
echo "repo.url=nomsg" &&
echo "repo.path=$PWD/repos/nomsg/.git"
} >nomsgrc &&
CGIT_CONFIG="$PWD/nomsgrc" QUERY_STRING="url=nomsg/log/&showmsg=1" cgit >tmp &&
grep "no commit message" tmp &&
CGIT_CONFIG="$PWD/nomsgrc" QUERY_STRING="url=nomsg/" cgit >tmp &&
grep "no commit message" tmp
'
# A branch need not point at a commit. struct refinfo keeps taginfo and
# commitinfo in a union and fills only the member matching the object type,
# so sorting branches through the commit member read past the end of the
# smaller taginfo, and read NULL for a tree, which crashed. git update-ref
# refuses to create such a ref, hence the loose files written by hand below,
# and a repository is only files on disk so cgit meets whatever is there.
test_expect_success 'set up a repo whose branches point at odd objects' '
mkrepo repos/oddref 2 &&
(
cd repos/oddref &&
git tag -a annotated -m note &&
git rev-parse annotated >.git/refs/heads/points-at-tag &&
git rev-parse HEAD^{tree} >.git/refs/heads/points-at-tree &&
git for-each-ref refs/heads/ >refs.out &&
grep -q "tree.refs/heads/points-at-tree" refs.out &&
grep -q "tag.refs/heads/points-at-tag" refs.out
) &&
{
echo "virtual-root=/" &&
echo "cache-size=0" &&
echo "branch-sort=age" &&
echo "repo.url=oddref" &&
echo "repo.path=$PWD/repos/oddref/.git"
} >oddrefrc
'
test_expect_success 'refs page sorts such branches without crashing' '
CGIT_CONFIG="$PWD/oddrefrc" QUERY_STRING="url=oddref/refs/" cgit >tmp &&
grep "points-at-tree" tmp &&
grep "