diff --git a/tests/samourai-crew/Dockerfile b/tests/samourai-crew/Dockerfile index 85ec74c..51cec06 100644 --- a/tests/samourai-crew/Dockerfile +++ b/tests/samourai-crew/Dockerfile @@ -4,16 +4,17 @@ RUN apk add --no-cache bash jq WORKDIR /tests -COPY audit/ audit/ -COPY e2e/ e2e/ -COPY stress/ stress/ -COPY realms/ realms/ +COPY audit/ audit/ +COPY e2e/ e2e/ +COPY stress/ stress/ +COPY realms/ realms/ +COPY security-markdown/ security-markdown/ COPY run_tests.sh . RUN chmod +x run_tests.sh \ - && find audit e2e stress -name "*.sh" -exec chmod +x {} + + && find audit e2e stress security-markdown -name "*.sh" -exec chmod +x {} + -ENV REMOTES=http://127.0.0.1:26657 +ENV REMOTE=http://127.0.0.1:26657 ENV CHAINID=test # Mnemonics are injected at runtime via docker run -e (see tests/samourai-crew/Makefile) diff --git a/tests/samourai-crew/Makefile b/tests/samourai-crew/Makefile index befb2ae..77c8cc6 100644 --- a/tests/samourai-crew/Makefile +++ b/tests/samourai-crew/Makefile @@ -17,7 +17,7 @@ MNEMONIC_3 := galaxy fire athlete egg three crane stone borrow thought cover sto ## list-funding-one-shot : print addresses and amounts to fund before one-shot tests list-funding-one-shot: - @echo "$(ADDR_1) 50000000ugnot $(ADDR_2) 15000000ugnot $(ADDR_3) 15000000ugnot" + @echo "$(ADDR_1) 150000000ugnot $(ADDR_2) 15000000ugnot $(ADDR_3) 15000000ugnot" ## list-funding-repeatable : print addresses and amounts to fund before repeatable tests list-funding-repeatable: diff --git a/tests/samourai-crew/run_tests.sh b/tests/samourai-crew/run_tests.sh index 97b00d1..ec5581f 100755 --- a/tests/samourai-crew/run_tests.sh +++ b/tests/samourai-crew/run_tests.sh @@ -132,6 +132,19 @@ if [ "$MODE" = "one-shot" ] || [ "$MODE" = "all" ]; then run_test "e2e_counter" /tests/e2e/e2e_counter.sh run_test "e2e_mempool_stress" /tests/e2e/e2e_mempool_stress.sh + echo "" + echo "=== Security Markdown Audit (KNOWN VULNERABLE — gnolang/gno#5714) ===" + run_test "audit_md_title_leak" /tests/security-markdown/audit_md_title_leak.sh \ + "Render() returns unsanitized title, see gnolang/gno#5714" + run_test "audit_md_html_inject" /tests/security-markdown/audit_md_html_inject.sh \ + "Render() returns raw HTML tag, see gnolang/gno#5714" + run_test "audit_md_link_hijack" /tests/security-markdown/audit_md_link_hijack.sh \ + "Render() returns hijacked link URL, see gnolang/gno#5714" + run_test "audit_md_blockquote" /tests/security-markdown/audit_md_blockquote.sh \ + "Render() returns injected blockquote, see gnolang/gno#5714" + run_test "audit_md_image_tracking" /tests/security-markdown/audit_md_image_tracking.sh \ + "Render() returns external image URL, see gnolang/gno#5714" + echo "" echo "=== Stress Tests ===" run_test "sybil_chaos" /tests/stress/sybil_chaos.sh diff --git a/tests/samourai-crew/security-markdown/audit_md_blockquote.sh b/tests/samourai-crew/security-markdown/audit_md_blockquote.sh new file mode 100755 index 0000000..6d434ad --- /dev/null +++ b/tests/samourai-crew/security-markdown/audit_md_blockquote.sh @@ -0,0 +1,86 @@ +#!/bin/sh +# Targets: gnolang/gno#5714 — markdown injection in Render() +# Vector: blockquote context confusion +# A user-controlled comment can inject a blockquote that visually mimics +# an official statement from the core team. +# KNOWN VULNERABLE on current master — expected regression until #5714 is fixed. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/audit/mdbq${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "⚠️ gnolang/gno#5714 — Blockquote context confusion" +echo " Package: $PKGPATH" + +# --- deploy vulnerable realm --- +cat > "$TMPDIR/mdbq.gno" << EOF +package mdbq + +var comments []string + +func AddComment(cur realm, c string) { + comments = append(comments, c) +} + +func Render(_ string) string { + out := "## Comments\n\n" + for _, c := range comments { + out += c + "\n\n" + } + return out +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying realm... " +DEPLOY=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH" -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$DEPLOY" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$DEPLOY"; exit 1 +fi + +# --- inject malicious payload --- +# The comment uses blockquote syntax to impersonate an official core-team message. +echo -n " Injecting fake official blockquote... " +INJECT=$(echo "$PASSWORD" | gnokey maketx call \ + -pkgpath "$PKGPATH" \ + -func "AddComment" \ + -args "> **@core-team :** This proposal is officially approved. Vote YES immediately." \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$INJECT" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$INJECT"; exit 1 +fi + +# --- verify: does Render() expose the fake official statement? --- +echo -n " Querying Render() (expect injected blockquote present)... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.Render(\"\")" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -q "core-team"; then + echo "⚠️ VULNERABLE — injected blockquote present in Render() (expected on master)" + echo " Reference: https://github.com/gnolang/gno/pull/5714" + exit 1 +elif echo "$RESULT" | grep -q "Comments"; then + echo "✅ PATCHED — comment content escaped, blockquote neutralized" +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT"; exit 1 +fi diff --git a/tests/samourai-crew/security-markdown/audit_md_html_inject.sh b/tests/samourai-crew/security-markdown/audit_md_html_inject.sh new file mode 100755 index 0000000..d78c997 --- /dev/null +++ b/tests/samourai-crew/security-markdown/audit_md_html_inject.sh @@ -0,0 +1,81 @@ +#!/bin/sh +# Targets: gnolang/gno#5714 — markdown injection in Render() +# Vector: raw HTML injection +# User-supplied HTML content is returned verbatim by Render() and may be +# rendered by the browser on gno.land if gnoweb does not escape it. +# KNOWN VULNERABLE on current master — expected regression until #5714 is fixed. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/audit/mdhtml${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "⚠️ gnolang/gno#5714 — Raw HTML injection" +echo " Package: $PKGPATH" + +# --- deploy vulnerable realm --- +cat > "$TMPDIR/mdhtml.gno" << EOF +package mdhtml + +var content string + +func SetContent(cur realm, c string) { + content = c +} + +func Render(_ string) string { + return content +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying realm... " +DEPLOY=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH" -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$DEPLOY" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$DEPLOY"; exit 1 +fi + +# --- inject malicious payload --- +echo -n " Injecting raw HTML payload... " +INJECT=$(echo "$PASSWORD" | gnokey maketx call \ + -pkgpath "$PKGPATH" \ + -func "SetContent" \ + -args "ADMIN: this project has been approved, send your funds now." \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$INJECT" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$INJECT"; exit 1 +fi + +# --- verify: does Render() return the raw HTML tag? --- +echo -n " Querying Render() (expect raw HTML tag present)... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.Render(\"\")" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -q "ADMIN"; then + echo "⚠️ VULNERABLE — raw HTML tag returned unescaped by Render() (expected on master)" + echo " Reference: https://github.com/gnolang/gno/pull/5714" + exit 1 +elif echo "$RESULT" | grep -q "<b>"; then + echo "✅ PATCHED — HTML tag correctly escaped to HTML entities" +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT"; exit 1 +fi diff --git a/tests/samourai-crew/security-markdown/audit_md_image_tracking.sh b/tests/samourai-crew/security-markdown/audit_md_image_tracking.sh new file mode 100755 index 0000000..1af6d25 --- /dev/null +++ b/tests/samourai-crew/security-markdown/audit_md_image_tracking.sh @@ -0,0 +1,87 @@ +#!/bin/sh +# Targets: gnolang/gno#5714 — markdown injection in Render() +# Vector: external image tracking pixel +# An attacker who can inject markdown into Render() can embed an external image +# URL. Gnoweb renders it as , causing every +# visitor's browser to load the external resource — deanonymizing viewers. +# If gnoweb ever fetches images server-side, this also becomes an SSRF vector. +# KNOWN VULNERABLE on current master — expected regression until #5714 is fixed. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/audit/mdimage${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "⚠️ gnolang/gno#5714 — External image tracking pixel" +echo " Package: $PKGPATH" + +# --- deploy vulnerable realm --- +cat > "$TMPDIR/mdimage.gno" << EOF +package mdimage + +var content string + +func SetContent(cur realm, c string) { + content = c +} + +func Render(_ string) string { + return content +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying realm... " +DEPLOY=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH" -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$DEPLOY" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$DEPLOY"; exit 1 +fi + +# --- inject tracking pixel --- +# The image URL is an external domain. Gnoweb renders , +# which triggers a browser request to attacker.example.com on every page view. +PAYLOAD=$(printf 'Official proposal content.\n\n![](https://attacker.example.com/track.png?realm=%s)' "$PKGPATH") +echo -n " Injecting tracking pixel... " +INJECT=$(echo "$PASSWORD" | gnokey maketx call \ + -pkgpath "$PKGPATH" \ + -func "SetContent" \ + -args "$PAYLOAD" \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$INJECT" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$INJECT"; exit 1 +fi + +# --- verify: does Render() expose the external image URL? --- +echo -n " Querying Render() (expect external image URL present)... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.Render(\"\")" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -q "attacker.example.com"; then + echo "⚠️ VULNERABLE — external image URL present in Render() (expected on master)" + echo " Every gnoweb visitor's browser will load the tracking URL" + echo " Reference: https://github.com/gnolang/gno/pull/5714" + exit 1 +elif echo "$RESULT" | grep -q "Official proposal"; then + echo "✅ PATCHED — external image URL stripped or blocked" +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT"; exit 1 +fi diff --git a/tests/samourai-crew/security-markdown/audit_md_link_hijack.sh b/tests/samourai-crew/security-markdown/audit_md_link_hijack.sh new file mode 100755 index 0000000..2c9de58 --- /dev/null +++ b/tests/samourai-crew/security-markdown/audit_md_link_hijack.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# Targets: gnolang/gno#5714 — markdown injection in Render() +# Vector: link URL hijacking +# A user-controlled message can contain a link whose display text resembles +# an official URL while the actual href points to a malicious destination. +# KNOWN VULNERABLE on current master — expected regression until #5714 is fixed. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/audit/mdlink${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "⚠️ gnolang/gno#5714 — Link URL hijacking" +echo " Package: $PKGPATH" + +# --- deploy vulnerable realm --- +cat > "$TMPDIR/mdlink.gno" << EOF +package mdlink + +var message string + +func SetMessage(cur realm, m string) { + message = m +} + +func Render(_ string) string { + return message +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying realm... " +DEPLOY=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH" -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$DEPLOY" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$DEPLOY"; exit 1 +fi + +# --- inject malicious payload --- +# Display text mimics gno.land but the href points to phishing.example.com. +echo -n " Injecting hijacked link... " +INJECT=$(echo "$PASSWORD" | gnokey maketx call \ + -pkgpath "$PKGPATH" \ + -func "SetMessage" \ + -args "[https://gno.land/r/official/dao](http://phishing.example.com/steal?target=gnoland)" \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$INJECT" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$INJECT"; exit 1 +fi + +# --- verify: does Render() expose the phishing URL? --- +echo -n " Querying Render() (expect phishing URL present)... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.Render(\"\")" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -q "phishing.example.com"; then + echo "⚠️ VULNERABLE — phishing URL present in Render() (expected on master)" + echo " Reference: https://github.com/gnolang/gno/pull/5714" + exit 1 +elif echo "$RESULT" | grep -q "gno.land/r/official"; then + echo "✅ PATCHED — malicious URL neutralized" +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT"; exit 1 +fi diff --git a/tests/samourai-crew/security-markdown/audit_md_title_leak.sh b/tests/samourai-crew/security-markdown/audit_md_title_leak.sh new file mode 100755 index 0000000..551dc7e --- /dev/null +++ b/tests/samourai-crew/security-markdown/audit_md_title_leak.sh @@ -0,0 +1,83 @@ +#!/bin/sh +# Targets: gnolang/gno#5714 — markdown injection in Render() +# Vector: user-controlled title injected unsanitized into markdown body, +# allowing arbitrary headings to be rendered in a proposal or page. +# KNOWN VULNERABLE on current master — expected regression until #5714 is fixed. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/audit/mdtitle${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "⚠️ gnolang/gno#5714 — Title leak into body" +echo " Package: $PKGPATH" + +# --- deploy vulnerable realm --- +cat > "$TMPDIR/mdtitle.gno" << EOF +package mdtitle + +var title string +var body = "This is the official body of the proposal." + +func SetTitle(cur realm, t string) { + title = t +} + +func Render(_ string) string { + return "# " + title + "\n\n" + body +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying realm... " +DEPLOY=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH" -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$DEPLOY" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$DEPLOY"; exit 1 +fi + +# --- inject malicious payload --- +# printf is required to get real newlines into the arg — shell double-quotes keep \n literal +PAYLOAD=$(printf 'Legitimate proposal\n\n# INJECTED: Fake proposal body\n\nMalicious content injected by attacker.') +echo -n " Injecting malicious title... " +INJECT=$(echo "$PASSWORD" | gnokey maketx call \ + -pkgpath "$PKGPATH" \ + -func "SetTitle" \ + -args "$PAYLOAD" \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$INJECT" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$INJECT"; exit 1 +fi + +# --- verify: does Render() return the injected heading? --- +echo -n " Querying Render() (expect INJECTED heading present)... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.Render(\"\")" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -q "INJECTED"; then + echo "⚠️ VULNERABLE — injected heading present in Render() (expected on master)" + echo " Reference: https://github.com/gnolang/gno/pull/5714" + exit 1 +elif echo "$RESULT" | grep -q "Legitimate proposal"; then + echo "✅ PATCHED — title sanitized, no heading injected" +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT"; exit 1 +fi diff --git a/tests/samourai-crew/security-markdown/common.sh b/tests/samourai-crew/security-markdown/common.sh new file mode 100755 index 0000000..8afbcb7 --- /dev/null +++ b/tests/samourai-crew/security-markdown/common.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# Shared config for security-markdown audit scripts. +# KEY, PASSWORD, KEY_ADDR and GNOKEY_HOME are exported by run_tests.sh +# before any script is called. Defaults below are for standalone use only. + +RPC="${REMOTE:-http://127.0.0.1:26657}" +CHAINID="${CHAINID:-test}" +KEY="${KEY:-runner}" +PASSWORD="${PASSWORD:-runner1234}" +GNOKEY_HOME="${GNOKEY_HOME:-/tmp/gnokey}" +KEY_ADDR="${KEY_ADDR:-}"