Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-13.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run:
uses: ./.github/workflows/ci.yml
with:
remote: ${{ vars.REMOTE_TEST13 || 'https://rpc.test-13-aeddi-1.gnoland.network' }}
remote: ${{ vars.REMOTE_TEST13 || 'https://rpc.test13.testnets.gno.land' }}
chain_id: ${{ vars.CHAINID_TEST13 || 'test-13' }}
funder_script: ./funders/test-13.sh
test_type: ${{ inputs.test_type || 'both' }}
2 changes: 1 addition & 1 deletion tests/samourai-crew/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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) 150000000ugnot $(ADDR_2) 15000000ugnot $(ADDR_3) 15000000ugnot"
@echo "$(ADDR_1) 250000000ugnot $(ADDR_2) 15000000ugnot $(ADDR_3) 15000000ugnot"

## list-funding-repeatable : print addresses and amounts to fund before repeatable tests
list-funding-repeatable:
Expand Down
119 changes: 119 additions & 0 deletions tests/samourai-crew/e2e/e2e_boards2_delete.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/bin/sh
# Tests that DeleteReply leaves no ghost entry in the thread render (PR #5759).
#
# Prerequisite: CreateBoard requires PermissionBoardCreate on the realm-level
# permissions (gPerms), granted only to the "admin"/"owner" role (boards.gno:56,
# permissions.gno). The runner account needs a one-time grant:
# InviteMember(0, <runner-addr>, "admin")
# called by an existing realm admin/owner (gPerms RoleOwner). Without it, this
# script fails with "unauthorized ... CreateBoard at public.gno:146".

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=../audit/common.sh
. "$SCRIPT_DIR/../audit/common.sh"

SUFFIX=$(date +%s)
BOARD_NAME="samcrew-test-delete${SUFFIX}"
REPLY_BODY="Ghost test reply — should disappear after delete."

echo "🗑 BOARDS2 DELETE TEST"

# --- Setup: board + thread + reply ---
echo -n " Creating board '${BOARD_NAME}'... "
OUT=$(echo "$PASSWORD" | gnokey maketx call \
-pkgpath "gno.land/r/gnoland/boards2/v1" \
-func "CreateBoard" \
-args "$BOARD_NAME" -args "true" -args "true" \
-gas-fee 5000000ugnot -gas-wanted 100000000 \
-broadcast -chainid "$CHAINID" -remote "$RPC" \
-insecure-password-stdin \
-home "$GNOKEY_HOME" \
"$KEY" 2>&1)
if echo "$OUT" | grep -q "OK!"; then echo "OK"; else
echo "FAILED"; echo "$OUT"; exit 1
fi

BOARD_ID=$(gnokey query vm/qeval \
-data "gno.land/r/gnoland/boards2/v1.GetBoardIDFromName(\"${BOARD_NAME}\")" \
-remote "$RPC" 2>&1 | grep -oE '^\([0-9]+' | tr -d '(')
[ -z "$BOARD_ID" ] && echo "FAILED: could not get board ID" && exit 1

echo -n " Creating thread... "
OUT=$(echo "$PASSWORD" | gnokey maketx call \
-pkgpath "gno.land/r/gnoland/boards2/v1" \
-func "CreateThread" \
-args "$BOARD_ID" -args "Delete test thread" -args "Thread body for delete test." \
-gas-fee 5000000ugnot -gas-wanted 100000000 \
-broadcast -chainid "$CHAINID" -remote "$RPC" \
-insecure-password-stdin \
-home "$GNOKEY_HOME" \
"$KEY" 2>&1)
if echo "$OUT" | grep -q "OK!"; then echo "OK"; else
echo "FAILED"; echo "$OUT"; exit 1
fi

THREAD_ID=$(gnokey query vm/qrender \
-data "gno.land/r/gnoland/boards2/v1:${BOARD_NAME}" \
-remote "$RPC" 2>&1 | grep -oE "${BOARD_NAME}/[0-9]+" | grep -oE '[0-9]+$' | head -1)
[ -z "$THREAD_ID" ] && echo "FAILED: could not get thread ID" && exit 1
echo " Thread ID: $THREAD_ID"

echo -n " Creating reply... "
OUT=$(echo "$PASSWORD" | gnokey maketx call \
-pkgpath "gno.land/r/gnoland/boards2/v1" \
-func "CreateReply" \
-args "$BOARD_ID" -args "$THREAD_ID" -args "$THREAD_ID" \
-args "$REPLY_BODY" \
-gas-fee 5000000ugnot -gas-wanted 100000000 \
-broadcast -chainid "$CHAINID" -remote "$RPC" \
-insecure-password-stdin \
-home "$GNOKEY_HOME" \
"$KEY" 2>&1)
if echo "$OUT" | grep -q "OK!"; then echo "OK"; else
echo "FAILED"; echo "$OUT"; exit 1
fi

# Get reply ID from thread render
REPLY_ID=$(gnokey query vm/qrender \
-data "gno.land/r/gnoland/boards2/v1:${BOARD_NAME}/${THREAD_ID}" \
-remote "$RPC" 2>&1 | grep -oE "${BOARD_NAME}/${THREAD_ID}/[0-9]+" | grep -oE '[0-9]+$' | head -1)
[ -z "$REPLY_ID" ] && echo "FAILED: could not parse reply ID from thread render" && exit 1
echo " Reply ID: $REPLY_ID"

# --- Verify reply is present before delete ---
echo -n " Verifying reply is visible before delete... "
RENDER_BEFORE=$(gnokey query vm/qrender \
-data "gno.land/r/gnoland/boards2/v1:${BOARD_NAME}/${THREAD_ID}" \
-remote "$RPC" 2>&1)
if echo "$RENDER_BEFORE" | grep -qF "Ghost test reply"; then echo "OK"; else
echo "FAILED — reply not visible before delete"; echo "$RENDER_BEFORE"; exit 1
fi

# --- DeleteReply ---
echo -n " Deleting reply... "
OUT=$(echo "$PASSWORD" | gnokey maketx call \
-pkgpath "gno.land/r/gnoland/boards2/v1" \
-func "DeleteReply" \
-args "$BOARD_ID" -args "$THREAD_ID" -args "$REPLY_ID" \
-gas-fee 5000000ugnot -gas-wanted 100000000 \
-broadcast -chainid "$CHAINID" -remote "$RPC" \
-insecure-password-stdin \
-home "$GNOKEY_HOME" \
"$KEY" 2>&1)
if echo "$OUT" | grep -q "OK!"; then echo "OK"; else
echo "FAILED"; echo "$OUT"; exit 1
fi

# --- Verify reply is gone after delete ---
echo -n " Verifying reply is gone after delete (no ghost)... "
RENDER_AFTER=$(gnokey query vm/qrender \
-data "gno.land/r/gnoland/boards2/v1:${BOARD_NAME}/${THREAD_ID}" \
-remote "$RPC" 2>&1)
if echo "$RENDER_AFTER" | grep -qF "Ghost test reply"; then
echo "FAILED — ghost entry detected after delete"
echo "$RENDER_AFTER"
exit 1
else
echo "OK"
echo "✅ BOARDS2 DELETE OK — no ghost entry"
fi
104 changes: 104 additions & 0 deletions tests/samourai-crew/e2e/e2e_boards2_flow.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/bin/sh
# Tests the basic boards2 lifecycle on the live chain:
# CreateBoard -> CreateThread -> CreateReply -> qrender verification.
#
# Prerequisite: CreateBoard requires PermissionBoardCreate on the realm-level
# permissions (gPerms), granted only to the "admin"/"owner" role (boards.gno:56,
# permissions.gno). The runner account needs a one-time grant:
# InviteMember(0, <runner-addr>, "admin")
# called by an existing realm admin/owner (gPerms RoleOwner). Without it, this
# script fails with "unauthorized ... CreateBoard at public.gno:146".

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=../audit/common.sh
. "$SCRIPT_DIR/../audit/common.sh"

SUFFIX=$(date +%s)
BOARD_NAME="samcrew-test-flow${SUFFIX}"

echo "🏛 BOARDS2 FLOW TEST"

# --- CreateBoard ---
echo -n " Creating board '${BOARD_NAME}'... "
OUT=$(echo "$PASSWORD" | gnokey maketx call \
-pkgpath "gno.land/r/gnoland/boards2/v1" \
-func "CreateBoard" \
-args "$BOARD_NAME" -args "true" -args "true" \
-gas-fee 5000000ugnot -gas-wanted 100000000 \
-broadcast -chainid "$CHAINID" -remote "$RPC" \
-insecure-password-stdin \
-home "$GNOKEY_HOME" \
"$KEY" 2>&1)
if echo "$OUT" | grep -q "OK!"; then echo "OK"; else
echo "FAILED"; echo "$OUT"; exit 1
fi

# --- Get BoardID ---
BOARD_ID=$(gnokey query vm/qeval \
-data "gno.land/r/gnoland/boards2/v1.GetBoardIDFromName(\"${BOARD_NAME}\")" \
-remote "$RPC" 2>&1 | grep -oE '^\([0-9]+' | tr -d '(')
if [ -z "$BOARD_ID" ]; then
echo " FAILED: could not get board ID"; exit 1
fi
echo " Board ID: $BOARD_ID"

# --- CreateThread ---
echo -n " Creating thread... "
OUT=$(echo "$PASSWORD" | gnokey maketx call \
-pkgpath "gno.land/r/gnoland/boards2/v1" \
-func "CreateThread" \
-args "$BOARD_ID" -args "Hello boards2" -args "This is a test thread body." \
-gas-fee 5000000ugnot -gas-wanted 100000000 \
-broadcast -chainid "$CHAINID" -remote "$RPC" \
-insecure-password-stdin \
-home "$GNOKEY_HOME" \
"$KEY" 2>&1)
if echo "$OUT" | grep -q "OK!"; then echo "OK"; else
echo "FAILED"; echo "$OUT"; exit 1
fi

# --- Get ThreadID from board render ---
THREAD_ID=$(gnokey query vm/qrender \
-data "gno.land/r/gnoland/boards2/v1:${BOARD_NAME}" \
-remote "$RPC" 2>&1 | grep -oE "${BOARD_NAME}/[0-9]+" | grep -oE '[0-9]+$' | head -1)
if [ -z "$THREAD_ID" ]; then
echo " FAILED: could not parse thread ID from board render"; exit 1
fi
echo " Thread ID: $THREAD_ID"

# --- Verify board render shows thread title ---
echo -n " Verifying board render shows thread... "
RENDER=$(gnokey query vm/qrender \
-data "gno.land/r/gnoland/boards2/v1:${BOARD_NAME}" \
-remote "$RPC" 2>&1)
if echo "$RENDER" | grep -q "Hello boards2"; then echo "OK"; else
echo "FAILED"; echo "$RENDER"; exit 1
fi

# --- CreateReply ---
echo -n " Creating reply... "
OUT=$(echo "$PASSWORD" | gnokey maketx call \
-pkgpath "gno.land/r/gnoland/boards2/v1" \
-func "CreateReply" \
-args "$BOARD_ID" -args "$THREAD_ID" -args "$THREAD_ID" \
-args "This is a test reply." \
-gas-fee 5000000ugnot -gas-wanted 100000000 \
-broadcast -chainid "$CHAINID" -remote "$RPC" \
-insecure-password-stdin \
-home "$GNOKEY_HOME" \
"$KEY" 2>&1)
if echo "$OUT" | grep -q "OK!"; then echo "OK"; else
echo "FAILED"; echo "$OUT"; exit 1
fi

# --- Verify thread render shows reply ---
echo -n " Verifying thread render shows reply... "
THREAD_RENDER=$(gnokey query vm/qrender \
-data "gno.land/r/gnoland/boards2/v1:${BOARD_NAME}/${THREAD_ID}" \
-remote "$RPC" 2>&1)
if echo "$THREAD_RENDER" | grep -q "This is a test reply"; then
echo "OK"
echo "✅ BOARDS2 FLOW OK"
else
echo "FAILED"; echo "$THREAD_RENDER"; exit 1
fi
137 changes: 137 additions & 0 deletions tests/samourai-crew/e2e/e2e_boards2_security.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/bin/sh
# Tests the gno-foreign markdown sandbox (PR #5759).
# Verifies that escape tricks in reply bodies do not break the sandbox wrapper.
#
# Prerequisite: CreateBoard requires PermissionBoardCreate on the realm-level
# permissions (gPerms), granted only to the "admin"/"owner" role (boards.gno:56,
# permissions.gno). The runner account needs a one-time grant:
# InviteMember(0, <runner-addr>, "admin")
# called by an existing realm admin/owner (gPerms RoleOwner). Without it, this
# script fails with "unauthorized ... CreateBoard at public.gno:146".

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=../audit/common.sh
. "$SCRIPT_DIR/../audit/common.sh"

SUFFIX=$(date +%s)
BOARD_NAME="samcrew-test-security${SUFFIX}"
PASS_COUNT=0
FAIL_COUNT=0

check() {
LABEL="$1"
CONDITION="$2"
if $CONDITION; then
echo " [OK] $LABEL"
PASS_COUNT=$((PASS_COUNT + 1))
else
echo " [FAIL] $LABEL"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
}

echo "🔒 BOARDS2 SECURITY TEST"

# --- Setup: board + thread ---
echo -n " Creating board '${BOARD_NAME}'... "
OUT=$(echo "$PASSWORD" | gnokey maketx call \
-pkgpath "gno.land/r/gnoland/boards2/v1" \
-func "CreateBoard" \
-args "$BOARD_NAME" -args "true" -args "true" \
-gas-fee 5000000ugnot -gas-wanted 100000000 \
-broadcast -chainid "$CHAINID" -remote "$RPC" \
-insecure-password-stdin \
-home "$GNOKEY_HOME" \
"$KEY" 2>&1)
if echo "$OUT" | grep -q "OK!"; then echo "OK"; else
echo "FAILED"; echo "$OUT"; exit 1
fi

BOARD_ID=$(gnokey query vm/qeval \
-data "gno.land/r/gnoland/boards2/v1.GetBoardIDFromName(\"${BOARD_NAME}\")" \
-remote "$RPC" 2>&1 | grep -oE '^\([0-9]+' | tr -d '(')
[ -z "$BOARD_ID" ] && echo "FAILED: could not get board ID" && exit 1
echo " Board ID: $BOARD_ID"

echo -n " Creating thread... "
OUT=$(echo "$PASSWORD" | gnokey maketx call \
-pkgpath "gno.land/r/gnoland/boards2/v1" \
-func "CreateThread" \
-args "$BOARD_ID" -args "Security test thread" -args "Testing gno-foreign sandbox." \
-gas-fee 5000000ugnot -gas-wanted 100000000 \
-broadcast -chainid "$CHAINID" -remote "$RPC" \
-insecure-password-stdin \
-home "$GNOKEY_HOME" \
"$KEY" 2>&1)
if echo "$OUT" | grep -q "OK!"; then echo "OK"; else
echo "FAILED"; echo "$OUT"; exit 1
fi

THREAD_ID=$(gnokey query vm/qrender \
-data "gno.land/r/gnoland/boards2/v1:${BOARD_NAME}" \
-remote "$RPC" 2>&1 | grep -oE "${BOARD_NAME}/[0-9]+" | grep -oE '[0-9]+$' | head -1)
[ -z "$THREAD_ID" ] && echo "FAILED: could not get thread ID" && exit 1
echo " Thread ID: $THREAD_ID"

# --- Test 1: reply with </gno-foreign/> escape trick ---
echo -n " Posting reply with </gno-foreign/> escape trick... "
OUT=$(echo "$PASSWORD" | gnokey maketx call \
-pkgpath "gno.land/r/gnoland/boards2/v1" \
-func "CreateReply" \
-args "$BOARD_ID" -args "$THREAD_ID" -args "$THREAD_ID" \
-args 'Escape attempt: </gno-foreign/> injected content' \
-gas-fee 5000000ugnot -gas-wanted 100000000 \
-broadcast -chainid "$CHAINID" -remote "$RPC" \
-insecure-password-stdin \
-home "$GNOKEY_HOME" \
"$KEY" 2>&1)
if echo "$OUT" | grep -q "OK!"; then echo "OK"; else
echo "FAILED"; echo "$OUT"; exit 1
fi

RENDER=$(gnokey query vm/qrender \
-data "gno.land/r/gnoland/boards2/v1:${BOARD_NAME}/${THREAD_ID}" \
-remote "$RPC" 2>&1)

# The raw </gno-foreign/> must NOT appear as a standalone close tag in render output.
# After sanitization it should be escaped (e.g. &lt;/gno-foreign/&gt; or similar).
check "escape trick </gno-foreign/> is sanitized" \
'! echo "$RENDER" | grep -qF "</gno-foreign/>"'

# The render must still be parseable — the thread title must be present.
check "thread render is intact after escape trick" \
'echo "$RENDER" | grep -q "Security test thread"'

# --- Test 2: reply with javascript: link ---
echo -n " Posting reply with javascript: link... "
OUT=$(echo "$PASSWORD" | gnokey maketx call \
-pkgpath "gno.land/r/gnoland/boards2/v1" \
-func "CreateReply" \
-args "$BOARD_ID" -args "$THREAD_ID" -args "$THREAD_ID" \
-args '[click me](javascript:alert(1))' \
-gas-fee 5000000ugnot -gas-wanted 100000000 \
-broadcast -chainid "$CHAINID" -remote "$RPC" \
-insecure-password-stdin \
-home "$GNOKEY_HOME" \
"$KEY" 2>&1)
if echo "$OUT" | grep -q "OK!"; then echo "OK"; else
echo "FAILED"; echo "$OUT"; exit 1
fi

RENDER=$(gnokey query vm/qrender \
-data "gno.land/r/gnoland/boards2/v1:${BOARD_NAME}/${THREAD_ID}" \
-remote "$RPC" 2>&1)

# The raw markdown body is stored in the realm — link sanitization happens in gnoweb.
# We verify that the render is intact (not broken by the link content).
check "thread render intact after javascript: link reply" \
'echo "$RENDER" | grep -q "Security test thread"'

# --- Summary ---
echo ""
if [ "$FAIL_COUNT" -eq 0 ]; then
echo "✅ BOARDS2 SECURITY OK ($PASS_COUNT checks passed)"
else
echo "❌ BOARDS2 SECURITY: $FAIL_COUNT/$((PASS_COUNT + FAIL_COUNT)) checks FAILED"
exit 1
fi
9 changes: 9 additions & 0 deletions tests/samourai-crew/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,15 @@ if [ "$MODE" = "one-shot" ] || [ "$MODE" = "all" ]; then
run_test "e2e_cross_realm_callback" /tests/e2e/e2e_cross_realm_callback.sh
run_test "e2e_storage_metering" /tests/e2e/e2e_storage_metering.sh

echo ""
echo "=== Boards2 E2E Tests (gno.land/r/gnoland/boards2/v1, PR #5759) ==="
run_test "e2e_boards2_flow" /tests/e2e/e2e_boards2_flow.sh \
"" "requires realm-admin grant for runner: InviteMember(0, \$KEY_ADDR, \"admin\") on r/gnoland/boards2/v1, else CreateBoard panics with PermissionBoardCreate unauthorized"
run_test "e2e_boards2_security" /tests/e2e/e2e_boards2_security.sh \
"" "requires realm-admin grant for runner: InviteMember(0, \$KEY_ADDR, \"admin\") on r/gnoland/boards2/v1, else CreateBoard panics with PermissionBoardCreate unauthorized"
run_test "e2e_boards2_delete" /tests/e2e/e2e_boards2_delete.sh \
"" "requires realm-admin grant for runner: InviteMember(0, \$KEY_ADDR, \"admin\") on r/gnoland/boards2/v1, else CreateBoard panics with PermissionBoardCreate unauthorized"

echo ""
echo "=== Security Markdown Audit (PR #5714 merged — sanitize library opt-in) ==="
run_test "audit_md_title_leak" /tests/security-markdown/audit_md_title_leak.sh \
Expand Down
Loading