#!/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 "" tmp || return 1 done ' test_expect_success 'name-sorted branches are unaffected' ' sed "s/^branch-sort=age$/branch-sort=name/" oddrefrc >oddrefnamerc && cgit_rc oddrefnamerc "url=oddref/refs/heads/" >tmp && grep "points-at-tree" tmp && grep "" tmp ' # module-link is a template and a trusted scan lets a repository set its own # from a cgitrc in the tree, so handing that string to printf let the owner # of a scanned repository crash the process, or read stack memory into the # served page, merely by adding conversions past the two that are filled. # The tests after the fixture also pin down the templates that must keep # working. test_expect_success 'set up a submodule fixture with a hostile module-link' ' mkrepo repos/modlink 1 && sub=$(git -C repos/modlink rev-parse HEAD) && ( cd repos/modlink && git update-index --add --cacheinfo 160000,$sub,submod && git commit -m gitlink ) && mkdir -p scan && cp -R repos/modlink scan/modlink && printf "module-link=/m/%%s/%%s/%%s/%%s/%%s/%%s/%%s/%%s/%%s/%%s\n" >scan/modlink/.git/cgitrc && { echo "virtual-root=/" && echo "cache-size=0" && echo "trust-scan-config=1" && echo "scan-path=$PWD/scan" } >modlinkrc ' test_expect_success 'a surplus conversion does not crash the tree view' ' cgit_rc modlinkrc "url=modlink/tree/" >tmp && grep "ls-mod" tmp ' test_expect_success 'a surplus conversion is shown literally, not filled' ' grep "href=./m/submod/[0-9a-f]*/%s/%s/" tmp ' test_expect_success 'a well-formed module-link still takes name and sha1' ' cgitrc_repo modlinkokrc modlink "module-link=/mod/%s/commit/?id=%s" && cgit_rc modlinkokrc "url=modlink/tree/" >tmp && grep "href=./mod/submod/commit/?id=$sub." tmp ' test_expect_success 'a per-path module-link takes only the sha1' ' cgitrc_repo modlinkpathrc modlink "repo.module-link.submod=https://example.com/s/?id=%s" && cgit_rc modlinkpathrc "url=modlink/tree/" >tmp && grep "href=.https://example.com/s/?id=$sub." tmp ' test_expect_success 'a doubled percent in a module-link renders as one' ' cgitrc_repo modlinkpctrc modlink "module-link=/m/%s/%%/%s" && cgit_rc modlinkpctrc "url=modlink/tree/" >tmp && grep "href=./m/submod/%/$sub." tmp ' # An unknown page under a repository used to reach the page header with no # head resolved and crash in the branch switcher. test_expect_success 'an unknown page under a repository answers 404 with its header' ' cgit_url "foo/nonsense/" >tmp && grep "^Status: 404 Not Found" tmp && grep "Invalid request" tmp && grep ".gitmodules <<-EOF && [submodule "submod"] path = submod url = javascript:alert(1) [submodule "quoted"] path = quoted url = https://example.com/x'\''> EOF git add .gitmodules && git commit -m hostile ) && cgitrc_repo modlinkgmrc modlink enable-gitmodules-links=1 ' test_expect_success 'a javascript url never reaches an href' ' cgit_rc modlinkgmrc "url=modlink/tree/" >tmp && ! grep "href=.javascript:" tmp && grep "class=.ls-mod. title=.javascript:alert(1).>submod" tmp ' test_expect_success 'a quote in a web url cannot break out of the href' ' ! grep "" >x.svg && git add x.svg && git commit -m svg ) && cgitrc_repo svgrc svg mimetype.svg=image/svg+xml && cgit_ok svgrc "url=svg/plain/x.svg" >tmp && test_grep "^Content-Type: image/svg+xml;" tmp && test_grep "" tmp && for page in plain/x.svg blob/x.svg do cgit_ok svgrc "url=svg/$page" >tmp && test_grep "^X-Content-Type-Options: nosniff$" tmp && test_grep "^Content-Security-Policy: default-src .none.$" tmp || return 1 done && cgitrc_copy lscacherc && cgit_ok lscacherc "p=ls_cache" >tmp && test_grep "^Content-Type: text/plain;" tmp && test_grep "^X-Content-Type-Options: nosniff$" tmp && test_grep "^Content-Security-Policy: default-src .none.$" tmp ' test_done