|
1 | | -#!/bin/bash |
2 | | -# SQL Injection Protection Test |
| 1 | +#!/usr/bin/env bash |
| 2 | +# SQL Injection Protection Test (Quick) |
| 3 | +# |
| 4 | +# Phase 1 (unauthenticated): confirms endpoints reject unauthenticated requests. |
| 5 | +# Phase 2 (authenticated): confirms authenticated requests with SQLi payloads |
| 6 | +# do NOT trigger SQL errors or return leaked data. |
| 7 | +# |
| 8 | +# For comprehensive SQLi coverage (UNION, error-based, boolean, blind, time-based) |
| 9 | +# run: test-sqli-advanced.sh |
3 | 10 |
|
4 | | -API_URL="http://localhost:3333" |
| 11 | +API_URL="${API_URL:-http://localhost:3333}" |
| 12 | +API="${API_URL}/api/v1" |
| 13 | +TEST_EMAIL="${TEST_EMAIL:-test@prostaff.gg}" |
| 14 | +TEST_PASSWORD="${TEST_PASSWORD:-Test123!@#}" |
5 | 15 | GREEN='\033[0;32m' |
6 | 16 | RED='\033[0;31m' |
| 17 | +YELLOW='\033[1;33m' |
7 | 18 | NC='\033[0m' |
8 | 19 |
|
9 | | -echo "SQL Injection Protection Test" |
10 | | -echo "==============================" |
| 20 | +echo "SQL Injection Protection Test (Quick)" |
| 21 | +echo "======================================" |
11 | 22 | echo "" |
12 | 23 |
|
13 | 24 | PASSED=0 |
14 | 25 | FAILED=0 |
| 26 | +WARNINGS=0 |
15 | 27 |
|
16 | | -test_result() { |
17 | | - if [ "$1" = "PASS" ]; then |
18 | | - echo -e "${GREEN}[PASS]${NC} $2" |
19 | | - PASSED=$((PASSED + 1)) |
20 | | - else |
21 | | - echo -e "${RED}[FAIL]${NC} $2" |
22 | | - FAILED=$((FAILED + 1)) |
23 | | - fi |
| 28 | +pass() { echo -e "${GREEN}[PASS]${NC} $1"; PASSED=$(( PASSED + 1 )); } |
| 29 | +fail() { echo -e "${RED}[FAIL]${NC} $1"; FAILED=$(( FAILED + 1 )); } |
| 30 | +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; WARNINGS=$(( WARNINGS + 1 )); } |
| 31 | + |
| 32 | +has_sql_error() { |
| 33 | + echo "$1" | grep -qiE '(PG::|pg::|syntax error at or near|unterminated quoted|SQLSTATE|ORA-[0-9]{5})' |
24 | 34 | } |
25 | 35 |
|
26 | | -# All these should return 401 (need auth) or 400/422 (validation error) |
27 | | -# NOT 500 (SQL error) or 200 with leaked data |
| 36 | +# --------------------------------------------------------------------------- |
| 37 | +# Phase 1: Unauthenticated (must return 401, not 500 or SQL error) |
| 38 | +# --------------------------------------------------------------------------- |
| 39 | +echo "--- Phase 1: Unauthenticated requests ---" |
| 40 | +echo "" |
28 | 41 |
|
29 | | -echo "[1/4] Testing SQL injection in search parameter..." |
30 | | -RESULT=$(curl -s -w "\n%{http_code}" "$API_URL/api/v1/players?search=admin'%20OR%20'1'='1") |
| 42 | +echo "[1/4] Search param (no auth)..." |
| 43 | +RESULT=$(curl -s -w "\n%{http_code}" "${API}/players?search=admin'%20OR%20'1'='1") |
31 | 44 | HTTP_CODE=$(echo "$RESULT" | tail -n1) |
32 | 45 | BODY=$(echo "$RESULT" | head -n-1) |
33 | | - |
34 | 46 | if [ "$HTTP_CODE" = "401" ]; then |
35 | | - test_result "PASS" "Search parameter protected (requires auth)" |
36 | | -elif [ "$HTTP_CODE" = "500" ] || echo "$BODY" | grep -qi "syntax error\|pg::"; then |
37 | | - test_result "FAIL" "SQL injection may be possible (HTTP $HTTP_CODE)" |
| 47 | + pass "Unauthenticated search blocked (401)" |
| 48 | +elif has_sql_error "$BODY" || [ "$HTTP_CODE" = "500" ]; then |
| 49 | + fail "SQL error or 500 without auth (HTTP $HTTP_CODE)" |
38 | 50 | else |
39 | | - test_result "PASS" "Search parameter handled safely (HTTP $HTTP_CODE)" |
| 51 | + warn "Unexpected HTTP $HTTP_CODE without auth — verify response" |
40 | 52 | fi |
41 | 53 |
|
42 | | -echo "[2/4] Testing SQL injection in login..." |
43 | | -RESULT=$(curl -s -w "\n%{http_code}" -X POST "$API_URL/api/v1/auth/login" \ |
| 54 | +echo "[2/4] Login SQLi (no auth required)..." |
| 55 | +RESULT=$(curl -s -w "\n%{http_code}" -X POST "${API}/auth/login" \ |
44 | 56 | -H "Content-Type: application/json" \ |
45 | 57 | -d "{\"email\":\"admin' OR '1'='1\",\"password\":\"test\"}") |
46 | 58 | HTTP_CODE=$(echo "$RESULT" | tail -n1) |
47 | 59 | BODY=$(echo "$RESULT" | head -n-1) |
48 | | - |
49 | 60 | if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "422" ]; then |
50 | | - test_result "PASS" "Login protected against SQL injection" |
51 | | -elif [ "$HTTP_CODE" = "500" ] || echo "$BODY" | grep -qi "syntax error\|pg::"; then |
52 | | - test_result "FAIL" "SQL injection may be possible in login" |
| 61 | + pass "Login SQLi rejected (HTTP $HTTP_CODE)" |
| 62 | +elif has_sql_error "$BODY" || [ "$HTTP_CODE" = "500" ]; then |
| 63 | + fail "SQL error or 500 on login SQLi (HTTP $HTTP_CODE)" |
53 | 64 | else |
54 | | - test_result "PASS" "Login handled safely (HTTP $HTTP_CODE)" |
| 65 | + warn "Login SQLi returned HTTP $HTTP_CODE — verify response manually" |
55 | 66 | fi |
56 | 67 |
|
57 | | -echo "[3/4] Testing UNION-based SQL injection..." |
58 | | -RESULT=$(curl -s -w "\n%{http_code}" "$API_URL/api/v1/players?role=top'%20UNION%20SELECT%20*%20FROM%20users--") |
| 68 | +echo "[3/4] UNION injection (no auth)..." |
| 69 | +RESULT=$(curl -s -w "\n%{http_code}" "${API}/players?role=top'%20UNION%20SELECT%20*%20FROM%20users--") |
59 | 70 | HTTP_CODE=$(echo "$RESULT" | tail -n1) |
60 | 71 | BODY=$(echo "$RESULT" | head -n-1) |
61 | | - |
62 | 72 | if [ "$HTTP_CODE" = "401" ]; then |
63 | | - test_result "PASS" "UNION injection blocked (requires auth)" |
64 | | -elif [ "$HTTP_CODE" = "500" ] || echo "$BODY" | grep -qi "syntax error\|pg::"; then |
65 | | - test_result "FAIL" "UNION injection may be possible" |
| 73 | + pass "UNION injection blocked without auth (401)" |
| 74 | +elif has_sql_error "$BODY" || [ "$HTTP_CODE" = "500" ]; then |
| 75 | + fail "SQL error or 500 on unauthenticated UNION (HTTP $HTTP_CODE)" |
66 | 76 | else |
67 | | - test_result "PASS" "UNION injection handled safely (HTTP $HTTP_CODE)" |
| 77 | + warn "HTTP $HTTP_CODE on UNION without auth — verify" |
68 | 78 | fi |
69 | 79 |
|
70 | | -echo "[4/4] Testing comment-based SQL injection..." |
71 | | -RESULT=$(curl -s -w "\n%{http_code}" "$API_URL/api/v1/players?role=top';--") |
| 80 | +echo "[4/4] Comment injection (no auth)..." |
| 81 | +RESULT=$(curl -s -w "\n%{http_code}" "${API}/players?role=top';--") |
72 | 82 | HTTP_CODE=$(echo "$RESULT" | tail -n1) |
73 | 83 | BODY=$(echo "$RESULT" | head -n-1) |
74 | | - |
75 | 84 | if [ "$HTTP_CODE" = "401" ]; then |
76 | | - test_result "PASS" "Comment injection blocked (requires auth)" |
77 | | -elif [ "$HTTP_CODE" = "500" ] || echo "$BODY" | grep -qi "syntax error\|pg::"; then |
78 | | - test_result "FAIL" "Comment injection may be possible" |
| 85 | + pass "Comment injection blocked without auth (401)" |
| 86 | +elif has_sql_error "$BODY" || [ "$HTTP_CODE" = "500" ]; then |
| 87 | + fail "SQL error or 500 on unauthenticated comment injection (HTTP $HTTP_CODE)" |
79 | 88 | else |
80 | | - test_result "PASS" "Comment injection handled safely (HTTP $HTTP_CODE)" |
| 89 | + warn "HTTP $HTTP_CODE on comment injection without auth — verify" |
81 | 90 | fi |
82 | 91 |
|
| 92 | +# --------------------------------------------------------------------------- |
| 93 | +# Phase 2: Authenticated (the real test) |
| 94 | +# --------------------------------------------------------------------------- |
83 | 95 | echo "" |
84 | | -echo "==================================" |
85 | | -echo "RESULTS" |
86 | | -echo "==================================" |
87 | | -echo "Tests run: $((PASSED + FAILED))" |
88 | | -echo -e "${GREEN}Passed: $PASSED${NC}" |
89 | | -echo -e "${RED}Failed: $FAILED${NC}" |
| 96 | +echo "--- Phase 2: Authenticated requests ---" |
90 | 97 | echo "" |
91 | 98 |
|
92 | | -if [ $FAILED -eq 0 ]; then |
93 | | - echo -e "${GREEN}✓ SQL Injection protection is SECURE${NC}" |
| 99 | +TOKEN="" |
| 100 | +TMP_AUTH="$(mktemp)" |
| 101 | +AUTH_CODE=$(curl -s -o "${TMP_AUTH}" -w "%{http_code}" --max-time 10 \ |
| 102 | + -X POST "${API}/auth/login" \ |
| 103 | + -H "Content-Type: application/json" \ |
| 104 | + -d "{\"email\":\"${TEST_EMAIL}\",\"password\":\"${TEST_PASSWORD}\"}" 2>/dev/null) || AUTH_CODE="error" |
| 105 | +TOKEN=$(python3 -c " |
| 106 | +import sys, json |
| 107 | +try: |
| 108 | + d = json.load(open('${TMP_AUTH}')) |
| 109 | + t = (d.get('access_token') or d.get('token') |
| 110 | + or d.get('data', {}).get('access_token') |
| 111 | + or d.get('data', {}).get('token') or '') |
| 112 | + print(t) |
| 113 | +except Exception: |
| 114 | + pass |
| 115 | +" 2>/dev/null) || TOKEN="" |
| 116 | +rm -f "${TMP_AUTH}" |
| 117 | + |
| 118 | +if [ -z "${TOKEN}" ]; then |
| 119 | + warn "Could not obtain auth token (HTTP ${AUTH_CODE}) — skipping Phase 2" |
| 120 | + warn "Start the API and check TEST_EMAIL / TEST_PASSWORD env vars" |
| 121 | +else |
| 122 | + echo "Auth token obtained. Running authenticated SQLi checks..." |
94 | 123 | echo "" |
95 | | - echo "Notes:" |
96 | | - echo "- Queries use parameterization" |
97 | | - echo "- Authentication required on endpoints" |
98 | | - echo "- No SQL errors leaked to client" |
| 124 | + |
| 125 | + PAYLOADS=( |
| 126 | + "top' OR '1'='1" |
| 127 | + "top' OR 1=1--" |
| 128 | + "top' UNION SELECT NULL,NULL,NULL--" |
| 129 | + "') UNION SELECT username||'_'||password FROM Users--" |
| 130 | + "top' AND 1=CAST(version() AS INTEGER)--" |
| 131 | + "top'; SELECT pg_sleep(1);--" |
| 132 | + "top' AND (SELECT pg_sleep(1))=''--" |
| 133 | + ) |
| 134 | + LABELS=( |
| 135 | + "Classic OR injection" |
| 136 | + "OR 1=1 with comment" |
| 137 | + "UNION column count probe" |
| 138 | + "UNION credential extraction" |
| 139 | + "Error-based (CAST version)" |
| 140 | + "Stacked pg_sleep" |
| 141 | + "Subquery pg_sleep" |
| 142 | + ) |
| 143 | + |
| 144 | + TOTAL="${#PAYLOADS[@]}" |
| 145 | + for (( i=0; i<TOTAL; i++ )); do |
| 146 | + payload="${PAYLOADS[$i]}" |
| 147 | + label="${LABELS[$i]}" |
| 148 | + ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${payload//\'/\\\'}', safe=''))" 2>/dev/null) |
| 149 | + TMP_RESP="$(mktemp)" |
| 150 | + START_MS=$(date +%s%3N) |
| 151 | + RESP=$(curl -s -o "${TMP_RESP}" -w "%{http_code}|%{time_total}" --max-time 8 \ |
| 152 | + -H "Authorization: Bearer ${TOKEN}" \ |
| 153 | + "${API}/players?role=${ENCODED}" 2>/dev/null) || RESP="error|0" |
| 154 | + END_MS=$(date +%s%3N) |
| 155 | + ELAPSED=$(( END_MS - START_MS )) |
| 156 | + HTTP_CODE="${RESP%%|*}" |
| 157 | + RESP_BODY=$(cat "${TMP_RESP}" 2>/dev/null) |
| 158 | + rm -f "${TMP_RESP}" |
| 159 | + |
| 160 | + if has_sql_error "${RESP_BODY}"; then |
| 161 | + fail "${label} -> HTTP ${HTTP_CODE} [SQL ERROR LEAKED]" |
| 162 | + elif echo "${RESP_BODY}" | grep -qiE '(\$2[aby]\$|password_digest|bcrypt)'; then |
| 163 | + fail "${label} -> HTTP ${HTTP_CODE} [CREDENTIAL DATA IN RESPONSE]" |
| 164 | + elif [ "${HTTP_CODE}" = "500" ]; then |
| 165 | + fail "${label} -> HTTP 500" |
| 166 | + elif [ "${ELAPSED}" -ge 900 ] && echo "${label}" | grep -qi "sleep"; then |
| 167 | + fail "${label} -> HTTP ${HTTP_CODE} ${ELAPSED}ms [POSSIBLE TIME INJECTION]" |
| 168 | + else |
| 169 | + pass "${label} -> HTTP ${HTTP_CODE} ${ELAPSED}ms" |
| 170 | + fi |
| 171 | + sleep 0.1 |
| 172 | + done |
| 173 | +fi |
| 174 | + |
| 175 | +echo "" |
| 176 | +echo "======================================" |
| 177 | +echo "RESULTS" |
| 178 | +echo "======================================" |
| 179 | +echo "Passed : $PASSED" |
| 180 | +echo "Failed : $FAILED" |
| 181 | +echo "Warnings: $WARNINGS" |
| 182 | +echo "" |
| 183 | + |
| 184 | +if [ "$FAILED" -gt 0 ]; then |
| 185 | + echo -e "${RED}VULNERABLE: $FAILED test(s) failed — run test-sqli-advanced.sh for full analysis${NC}" |
| 186 | + exit 1 |
| 187 | +elif [ "$WARNINGS" -gt 0 ]; then |
| 188 | + echo -e "${YELLOW}UNCERTAIN: $WARNINGS warning(s) — verify manually or run test-sqli-advanced.sh${NC}" |
99 | 189 | exit 0 |
100 | 190 | else |
101 | | - echo -e "${RED}✗ SQL Injection vulnerabilities detected!${NC}" |
102 | | - exit 1 |
| 191 | + echo -e "${GREEN}SECURE: All quick checks passed${NC}" |
| 192 | + echo "" |
| 193 | + echo "For deeper coverage (blind, time-based, second-order):" |
| 194 | + echo " bash test-sqli-advanced.sh" |
| 195 | + exit 0 |
103 | 196 | fi |
0 commit comments