#!/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"
'
# --- Fork feature: built-in help page ----------------------------------------
test_expect_success 'help tab appears on the index by default' '
cgit_query "" >tmp &&
grep "p=help" tmp
'
test_expect_success 'help page renders the workflow guide' '
cgit_query "p=help" >tmp &&
grep "Compare two points in history" tmp
'
test_expect_success 'enable-help=0 hides the tab and the page' '
{
echo "virtual-root=/" &&
echo "cache-size=0" &&
echo "enable-help=0" &&
echo "repo.url=sec" &&
echo "repo.path=$PWD/repos/sec/.git"
} >nohelprc &&
CGIT_CONFIG="$PWD/nohelprc" QUERY_STRING="" cgit >tmp &&
! grep "p=help" tmp &&
CGIT_CONFIG="$PWD/nohelprc" QUERY_STRING="p=help" cgit >tmp &&
grep "Status: 404" tmp
'
# --- 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
'
test_done