#!/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
# Writes a config to the file named first, rooted at / with the cache off,
# holding the settings named after it in order, for a site cgitrc_repo cannot
# describe, a scan or more than one repository say.
cgitrc_site() {
cgitrc_site_file=$1
shift
printf '%s\n' virtual-root=/ cache-size=0 "$@" >"$cgitrc_site_file"
}
# Fails unless the response in the file named second opens with the status
# named first and carries no other, as a page that fails once its headers are
# out answers a second time below them.
expect_status() {
sed -n 1p "$2" >status-line &&
test_grep "^Status: $1 " status-line &&
grep "^Status:" "$2" >status-lines &&
test_line_count = 1 status-lines
}
# The shipped server configs hand cgit the path as PATH_INFO, which the
# server has already decoded, so a hostile byte arrives there raw rather
# than through url=.
cgit_path() {
CGIT_CONFIG="$PWD/$1" PATH_INFO=$2 cgit
}
cgit_ok_path() {
(
PATH_INFO=$2 &&
export PATH_INFO &&
cgit_ok "$1" ""
)
}
# 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 small file. The blob limit is the lowest
# there is, 1 KB, so the 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 &&
git add -A &&
git commit -m fixtures
) &&
cgitrc_repo seccgitrc sec max-blob-size=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 &&
expect_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' '
cgitrc_site secreadmerc \
repo.url=md "repo.path=$PWD/repos/sec/.git" repo.readme=master:README.md \
repo.url=txt "repo.path=$PWD/repos/sec/.git" repo.readme=master:readme.txt &&
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
'
# 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
'
# 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 "