#!/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 ' # 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 && cgitrc_site secrootrc "root-readme=$PWD/root.html" && 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 ' # 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, which only a sanitizer build catches, 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. A branch with a newer commit tells # the two sort orders apart. test_expect_success REFFILES '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 checkout -q -b newer && test_tick && git commit -q --allow-empty -m newer && git checkout -q master && 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 && cgitrc_repo oddrefnamerc oddref branch-sort=name ' # The line holding a branch's row, so a test can compare the order of rows. branch_row() { grep -n "]*>$1" "$2" | cut -d: -f1 } test_expect_success REFFILES 'the refs, branch and summary pages sort such branches' ' for page in refs/ refs/heads/ "" do cgit_ok oddrefrc "url=oddref/$page" >tmp && test_grep "