Files
proxmox/scripts/omnl/omnl-office2-access-security-test.sh
defiQUG b3a8fe4496
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
chore: sync all changes to Gitea
- Config, docs, scripts, and backup manifests
- Submodule refs unchanged (m = modified content in submodules)

Made-with: Cursor
2026-03-02 11:37:34 -08:00

140 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Security test: OMNL-2 (office 2) user must not access other offices' data or achieve
# path traversal / command injection. See docs/04-configuration/mifos-omnl-central-bank/OMNL_OFFICE_2_ACCESS_SECURITY_TEST.md
# Set STRICT_OFFICE_LIST=1 to fail when GET /offices returns other offices or GET /offices/20 returns 200.
set -euo pipefail
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
if [ -f "${REPO_ROOT}/omnl-fineract/.env" ]; then set +u; source "${REPO_ROOT}/omnl-fineract/.env" 2>/dev/null || true; set -u; fi
BASE_URL="${OMNL_FINERACT_BASE_URL:-}"
TENANT="${OMNL_FINERACT_TENANT:-omnl}"
# Office-2 user (do NOT use app.omnl admin)
OFFICE2_USER="${OMNL_OFFICE2_TEST_USER:-shamrayan.admin}"
OFFICE2_PASS="${OMNL_OFFICE2_TEST_PASSWORD:-${OMNL_SHAMRAYAN_ADMIN_PASSWORD:-}}"
STRICT="${STRICT_OFFICE_LIST:-0}"
FAILED=0
if [ -z "$BASE_URL" ] || [ -z "$OFFICE2_PASS" ]; then
echo "Set OMNL_FINERACT_BASE_URL and either OMNL_OFFICE2_TEST_PASSWORD or OMNL_SHAMRAYAN_ADMIN_PASSWORD (office-2 user only)." >&2
exit 2
fi
CURL_OFFICE2=(-s -S -w "\n%{http_code}" -H "Fineract-Platform-TenantId: ${TENANT}" -H "Content-Type: application/json" -u "${OFFICE2_USER}:${OFFICE2_PASS}")
echo "=== OMNL-2 access security test ==="
echo "Base URL: $BASE_URL"
echo "User: $OFFICE2_USER"
echo ""
# --- 1. Data isolation: GET /offices; office 2 must be present ---
echo "[1] Data isolation: GET /offices (office 2 must be visible)..."
OFFICES_RESP=$(curl "${CURL_OFFICE2[@]}" "${BASE_URL}/offices" 2>/dev/null)
OFFICES_BODY=$(echo "$OFFICES_RESP" | sed '$d')
OFFICES_CODE=$(echo "$OFFICES_RESP" | tail -n1)
if [ "$OFFICES_CODE" = "401" ]; then
echo " ERROR: Invalid or missing credentials (HTTP 401). Set office-2 user and password." >&2
exit 2
fi
if [ "$OFFICES_CODE" != "200" ]; then
echo " FAIL: GET /offices returned HTTP $OFFICES_CODE" >&2
FAILED=1
else
OFFICE_IDS=$(echo "$OFFICES_BODY" | jq -r '.[].id // empty' 2>/dev/null || true)
HAS_OFFICE2=""
for id in $OFFICE_IDS; do
[ "$id" = "2" ] && HAS_OFFICE2=1
done
if [ -z "$HAS_OFFICE2" ]; then
echo " FAIL: Office 2 not in GET /offices response" >&2
FAILED=1
else
BAD_IDS=""
for id in $OFFICE_IDS; do
if [ "$id" != "1" ] && [ "$id" != "2" ]; then
BAD_IDS="${BAD_IDS} ${id}"
fi
done
if [ -n "$BAD_IDS" ] && [ "$STRICT" = "1" ]; then
echo " FAIL: Strict mode — office-2 user sees other offices:${BAD_IDS}" >&2
FAILED=1
elif [ -n "$BAD_IDS" ]; then
echo " OK: Office 2 visible (other offices also listed:${BAD_IDS}; set STRICT_OFFICE_LIST=1 to fail)"
else
echo " OK: Only offices 1 and 2 visible"
fi
fi
fi
# --- 2. Data isolation: GET /offices/20 (strict: must not return 200 with office 20) ---
echo "[2] Data isolation: GET /offices/20..."
OFF20_RESP=$(curl "${CURL_OFFICE2[@]}" "${BASE_URL}/offices/20" 2>/dev/null)
OFF20_CODE=$(echo "$OFF20_RESP" | tail -n1)
if [ "$OFF20_CODE" = "200" ]; then
OFF20_BODY=$(echo "$OFF20_RESP" | sed '$d')
if echo "$OFF20_BODY" | jq -e '.id == 20' >/dev/null 2>&1; then
if [ "$STRICT" = "1" ]; then
echo " FAIL: Strict mode — office-2 user can read office 20 by ID" >&2
FAILED=1
else
echo " OK: 200 with office 20 (set STRICT_OFFICE_LIST=1 to fail)"
fi
else
echo " OK: 200 but no office 20 data"
fi
else
echo " OK: HTTP $OFF20_CODE (access denied or not found)"
fi
# --- 2b. Data isolation: GET /clients?officeId=20 must not return other offices' clients ---
echo "[3] Data isolation: GET /clients?officeId=20 (must be 403 or empty)..."
CLIENTS_RESP=$(curl "${CURL_OFFICE2[@]}" "${BASE_URL}/clients?officeId=20" 2>/dev/null)
CLIENTS_CODE=$(echo "$CLIENTS_RESP" | tail -n1)
CLIENTS_BODY=$(echo "$CLIENTS_RESP" | sed '$d')
if [ "$CLIENTS_CODE" = "200" ]; then
# pageItems or top-level array
COUNT=$(echo "$CLIENTS_BODY" | jq -r 'if .pageItems then (.pageItems | length) else (if type == "array" then length else 0 end) end' 2>/dev/null || echo "0")
case "${COUNT:-0}" in
''|null) COUNT=0 ;;
esac
if [ "${COUNT:-0}" -gt 0 ] 2>/dev/null; then
echo " FAIL: Office-2 user can list clients for office 20 (count=$COUNT)" >&2
FAILED=1
else
echo " OK: No clients for office 20 returned"
fi
else
echo " OK: HTTP $CLIENTS_CODE (access denied or no data)"
fi
# --- 4. Command injection: response must not contain actual file/command output ---
echo "[4] Command injection: GET response (must not contain file/command output)..."
INJECT_RESP=$(curl "${CURL_OFFICE2[@]}" "${BASE_URL}/offices?locale=en" 2>/dev/null)
INJECT_BODY=$(echo "$INJECT_RESP" | sed '$d')
if echo "$INJECT_BODY" | grep -qE 'root:.*:0:0:|uid=[0-9]+\(.*\)\s+gid='; then
echo " FAIL: Response may contain command output or file content" >&2
FAILED=1
else
echo " OK: No command/file output in response"
fi
# --- 5. Path traversal: must not return server file content ---
echo "[5] Path traversal: GET with path-like param (must not return file content)..."
TRAVERSE_RESP=$(curl "${CURL_OFFICE2[@]}" -G --data-urlencode "dateFormat=../../../etc/passwd" "${BASE_URL}/offices" 2>/dev/null)
TRAVERSE_BODY=$(echo "$TRAVERSE_RESP" | sed '$d')
if echo "$TRAVERSE_BODY" | grep -qE 'root:.*:0:0:'; then
echo " FAIL: Response may contain file content (path traversal)" >&2
FAILED=1
else
echo " OK: No file content in response"
fi
echo ""
if [ $FAILED -eq 0 ]; then
echo "All OMNL-2 access security checks passed."
exit 0
else
echo "One or more checks FAILED. Do not treat office-2 access as safe until resolved." >&2
exit 1
fi