#!/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 behaviour'
. ./setup.sh
# Some cases below share one repository and one config, so the fixture
# carries everything they need at once, readmes holding markup that must not
# reach the page as markup and a subdirectory to sort ahead of the files.
# The blob limit is the lowest there is, 1 KB, so a small file shows that a
# blob under it is still served.
test_expect_success 'set up security fixtures' '
mkrepo repos/sec 1 &&
(
cd repos/sec &&
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
) &&
cgitrc_repo seccgitrc sec max-blob-size=1 enable-tree-group-dirs=1
'
# 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. The request check and the log page refuse it
# with the same words, so the page cannot tell which of them did.
test_expect_success 'log id=--output is refused without writing a file' '
cgitrc_copy nocacherc &&
cgit_rc nocacherc "url=foo/log&id=--output=$PWD/log-output" >tmp &&
test_grep "^Status: 400" tmp &&
test_grep "Invalid revision
" tmp &&
test_path_is_missing log-output
'
test_expect_success 'a valid id= still renders the log' '
sha=$(git -C repos/foo rev-parse HEAD) &&
cgit_ok_url "foo/log&id=$sha" >tmp &&
test_grep ">commit 5" tmp
'
test_expect_success 'a small blob is still served' '
cgit_ok seccgitrc "url=sec/plain/afile" >tmp &&
strip_headers body &&
echo top >expect &&
test_cmp expect body
'
# 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" &&
echo "repo.url=txt" &&
echo "repo.path=$PWD/repos/sec/.git" &&
echo "repo.readme=master:readme.txt"
} >secreadmerc &&
cgit_ok secreadmerc "url=md/about/" >tmp &&
test_grep "pre class=.plaintext.># Title$" tmp &&
test_grep "^<script>alert(1)</script>$" tmp &&
test_grep ! "" tmp
'
test_expect_success 'non-markdown readme without a filter is escaped as plain text' '
cgit_ok secreadmerc "url=txt/about/" >tmp &&
test_grep "pre class=.plaintext.><script>alert(2)</script>$" tmp &&
test_grep ! "" tmp
'
# root-readme is the site's own file rather than repository content, so it
# goes out as written, on an about page of its own.
test_expect_success 'root-readme without a filter is its own page, as written' '
echo "site" >root.html &&
{
echo "virtual-root=/" &&
echo "cache-size=0" &&
echo "root-readme=$PWD/root.html"
} >secrootrc &&
cgit_rc secrootrc "url=" >tmp &&
grep "about" tmp &&
! grep "site" tmp &&
cgit_rc secrootrc "p=about" >tmp &&
grep "^site$" 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, and nor does any other
# page.
test_expect_success 'diff options submit through a button' '
sha=$(git -C repos/foo rev-parse HEAD) &&
cgit_ok_url "foo/commit&id=$sha" >tmp &&
test_grep "type=.submit. value=.reload." tmp &&
test_grep ! " on[a-z]*=" tmp
'
test_expect_success 'no page carries an inline handler' '
for page in repolist: summary:foo/ log:foo/log/ tree:foo/tree/ diff:foo/diff/ \
refs:foo/refs/
do
cgit_ok_url "${page#*:}" >tmp &&
test_grep "data-page=.${page%%:*}." tmp &&
test_grep ! " on[a-z]*=" tmp || return 1
done
'
# Grouping directories ahead of files is behaviour this fork adds, so nothing
# upstream covers it.
test_expect_success 'tree groups directories before files' '
cgit_rc seccgitrc "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,
# or a hash or question mark, which the attribute escaping leaves alone and
# a browser would read as the end of the path. So the path is percent-encoded
# on its way into them.
test_expect_success 'ssdiff percent-encodes the file path in its line links' '
mkrepo repos/xss 1 &&
quoted=$(printf "x\047y.txt") &&
(
cd repos/xss &&
printf "a\nb\n" >"$quoted" &&
printf "a\nb\n" >"p#q?.txt" &&
git add -A &&
git commit -m add &&
printf "a\nc\n" >"$quoted" &&
printf "a\nc\n" >"p#q?.txt" &&
git commit -am change
) &&
cgitrc_repo xssrc xss &&
old=$(git -C repos/xss rev-parse HEAD~1) &&
new=$(git -C repos/xss rev-parse HEAD) &&
cgit_ok xssrc "url=xss/diff/&id=$new&ss=1" >tmp &&
for path in x%27y.txt p%23q%3F.txt
do
test_grep "2 | " tmp &&
test_grep "2 | " tmp || return 1
done
'
# 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"
) &&
cgitrc_repo nomsgrc nomsg &&
cgit_rc nomsgrc "url=nomsg/log/&showmsg=1" >tmp &&
grep "no commit message" tmp &&
cgit_rc nomsgrc "url=nomsg/" >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
) &&
cgitrc_repo oddrefrc oddref branch-sort=age
'
test_expect_success 'the refs, branch and summary pages sort such branches' '
for page in refs/ refs/heads/ ""
do
cgit_rc oddrefrc "url=oddref/$page" >tmp &&
grep "points-at-tree" tmp &&
grep "