#!/bin/sh test_description='Check security fixes and fork-specific behavior' . ./setup.sh # A repo with an oversized blob, readmes that carry markup, and a directory, # plus a config that pins a tiny blob limit and groups directories. 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; } # --- Argument injection through the log id= parameter ----------------------- # A tip beginning with a dash would be parsed as a git option, and # id=--output= would create or truncate an arbitrary file. 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 -------------------- 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" ' # --- Readme rendering escapes untrusted repository content ------------------ 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 ' # --- Auto-submitting selects carry no inline handlers ------------------------ # A Content-Security-Policy without unsafe-inline blocks inline onchange # handlers, so the forms mark their selects and cgit.js wires them up. test_expect_success 'diff option selects use the autosubmit marker' ' sha=$(git -C repos/foo rev-parse HEAD) && cgit_query "url=foo/commit&id=$sha" >tmp && grep "data-autosubmit" tmp && ! grep "onchange" tmp ' # --- Fork feature: directories are grouped before files in the tree --------- 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" ' # --- Side-by-side diff percent-encodes a file path into its links ----------- # A file name is repository content and may contain a quote, which would # otherwise break out of the href attribute of the line-number links. 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 ' # --- A commit with no message must not crash the history views -------------- 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 one # matching the object type. Sorting branches by reading the commit member read # past the end of the smaller taginfo, and read NULL for a tree, which crashed. # git update-ref refuses to write these, so the refs go in as loose files: a # repository is just files on disk and cgit reads 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 "" tmp ' test_expect_success 'the branch page sorts them without crashing' ' CGIT_CONFIG="$PWD/oddrefrc" QUERY_STRING="url=oddref/refs/heads/" cgit >tmp && grep "points-at-tree" tmp && grep "" tmp ' test_expect_success 'the summary page sorts them without crashing' ' CGIT_CONFIG="$PWD/oddrefrc" QUERY_STRING="url=oddref/" cgit >tmp && grep "points-at-tree" tmp && grep "" tmp ' test_expect_success 'name-sorted branches are unaffected' ' { echo "virtual-root=/" && echo "cache-size=0" && echo "branch-sort=name" && echo "repo.url=oddref" && echo "repo.path=$PWD/repos/oddref/.git" } >oddrefnamerc && CGIT_CONFIG="$PWD/oddrefnamerc" QUERY_STRING="url=oddref/refs/heads/" cgit >tmp && grep "points-at-tree" tmp && grep "" tmp ' # --- A repository cannot supply a printf format string ---------------------- # module-link is a template, and scan-path lets a repository set it through its # own cgitrc. Handing it to printf let a repo owner crash the process, or read # stack memory into the served page, with surplus conversions. test_expect_success 'set up a submodule fixture with a hostile module-link' ' mkrepo repos/modlink 1 && ( cd repos/modlink && sub=$(git rev-parse HEAD) && 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 "scan-path=$PWD/scan" } >modlinkrc ' test_expect_success 'a surplus conversion does not crash the tree view' ' CGIT_CONFIG="$PWD/modlinkrc" QUERY_STRING="url=modlink/tree/" cgit >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 path and sha1' ' { echo "virtual-root=/" && echo "cache-size=0" && echo "module-link=/mod/%s/commit/?id=%s" && echo "repo.url=modlink" && echo "repo.path=$PWD/repos/modlink/.git" } >modlinkokrc && sub=$(git -C repos/modlink rev-parse HEAD~1) && CGIT_CONFIG="$PWD/modlinkokrc" QUERY_STRING="url=modlink/tree/" cgit >tmp && grep "href=./mod/submod/commit/?id=$sub." tmp ' test_expect_success 'a per-path module-link takes only the sha1' ' { echo "virtual-root=/" && echo "cache-size=0" && echo "repo.url=modlink" && echo "repo.path=$PWD/repos/modlink/.git" && echo "repo.module-link.submod=https://example.com/s/?id=%s" } >modlinkpathrc && sub=$(git -C repos/modlink rev-parse HEAD~1) && CGIT_CONFIG="$PWD/modlinkpathrc" QUERY_STRING="url=modlink/tree/" cgit >tmp && grep "href=.https://example.com/s/?id=$sub." tmp ' test_expect_success 'a doubled percent in a module-link renders as one' ' { echo "virtual-root=/" && echo "cache-size=0" && echo "module-link=/m/%s/%%/%s" && echo "repo.url=modlink" && echo "repo.path=$PWD/repos/modlink/.git" } >modlinkpctrc && sub=$(git -C repos/modlink rev-parse HEAD~1) && CGIT_CONFIG="$PWD/modlinkpctrc" QUERY_STRING="url=modlink/tree/" cgit >tmp && grep "href=./m/submod/%/$sub." tmp ' test_done