#!/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