whitzard-claw 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -0
- package/bin/whitzard-tui.js +73 -0
- package/bin/whitzard-webui.js +67 -0
- package/dist/tui/tui.js +38733 -0
- package/dist/webui/index.html +1235 -0
- package/dist/webui/server.js +876 -0
- package/ioc/c2-ips.txt +25 -0
- package/ioc/file-hashes.txt +13 -0
- package/ioc/malicious-domains.txt +46 -0
- package/ioc/malicious-hashes.txt +5 -0
- package/ioc/malicious-publishers.txt +34 -0
- package/ioc/malicious-skill-patterns.txt +87 -0
- package/package.json +50 -0
- package/scripts/check/access_control.sh +183 -0
- package/scripts/check/credential_storage.sh +222 -0
- package/scripts/check/execution_sandbox.sh +502 -0
- package/scripts/check/memory_poisoning.sh +334 -0
- package/scripts/check/network_exposure.sh +479 -0
- package/scripts/check/resource_cost.sh +182 -0
- package/scripts/check/supply_chain.sh +553 -0
- package/scripts/repair/access_control/_common.sh +249 -0
- package/scripts/repair/access_control/check_1.sh +28 -0
- package/scripts/repair/access_control/check_2.sh +27 -0
- package/scripts/repair/access_control/check_3.sh +23 -0
- package/scripts/repair/access_control/check_4.sh +23 -0
- package/scripts/repair/access_control/check_5.sh +20 -0
- package/scripts/repair/credential_storage/_common.sh +277 -0
- package/scripts/repair/credential_storage/check_1.sh +47 -0
- package/scripts/repair/credential_storage/check_2.sh +35 -0
- package/scripts/repair/credential_storage/check_3.sh +53 -0
- package/scripts/repair/credential_storage/logs/security-scan.log +15 -0
- package/scripts/repair/execution_sandbox/_common.sh +302 -0
- package/scripts/repair/execution_sandbox/check_1.sh +67 -0
- package/scripts/repair/execution_sandbox/check_10.sh +23 -0
- package/scripts/repair/execution_sandbox/check_11.sh +34 -0
- package/scripts/repair/execution_sandbox/check_12.sh +38 -0
- package/scripts/repair/execution_sandbox/check_13.sh +29 -0
- package/scripts/repair/execution_sandbox/check_2.sh +46 -0
- package/scripts/repair/execution_sandbox/check_3.sh +37 -0
- package/scripts/repair/execution_sandbox/check_4.sh +23 -0
- package/scripts/repair/execution_sandbox/check_5.sh +28 -0
- package/scripts/repair/execution_sandbox/check_6.sh +17 -0
- package/scripts/repair/execution_sandbox/check_7.sh +17 -0
- package/scripts/repair/execution_sandbox/check_8.sh +17 -0
- package/scripts/repair/execution_sandbox/check_9.sh +17 -0
- package/scripts/repair/execution_sandbox/logs/security-scan.log +10 -0
- package/scripts/repair/memory_poisoning/_common.sh +336 -0
- package/scripts/repair/memory_poisoning/check_1.sh +51 -0
- package/scripts/repair/memory_poisoning/check_2.sh +26 -0
- package/scripts/repair/memory_poisoning/check_3.sh +24 -0
- package/scripts/repair/memory_poisoning/check_4.sh +27 -0
- package/scripts/repair/memory_poisoning/check_5.sh +20 -0
- package/scripts/repair/network_exposure/_common.sh +330 -0
- package/scripts/repair/network_exposure/check_1.sh +86 -0
- package/scripts/repair/network_exposure/check_10.sh +16 -0
- package/scripts/repair/network_exposure/check_11.sh +31 -0
- package/scripts/repair/network_exposure/check_12.sh +24 -0
- package/scripts/repair/network_exposure/check_2.sh +26 -0
- package/scripts/repair/network_exposure/check_3.sh +43 -0
- package/scripts/repair/network_exposure/check_4.sh +23 -0
- package/scripts/repair/network_exposure/check_5.sh +16 -0
- package/scripts/repair/network_exposure/check_6.sh +98 -0
- package/scripts/repair/network_exposure/check_7.sh +35 -0
- package/scripts/repair/network_exposure/check_8.sh +19 -0
- package/scripts/repair/network_exposure/check_9.sh +19 -0
- package/scripts/repair/resource_cost/_common.sh +303 -0
- package/scripts/repair/resource_cost/check_1.sh +16 -0
- package/scripts/repair/resource_cost/check_2.sh +16 -0
- package/scripts/repair/resource_cost/check_3.sh +23 -0
- package/scripts/repair/supply_chain/_common.sh +222 -0
- package/scripts/repair/supply_chain/check_1.sh +95 -0
- package/scripts/repair/supply_chain/check_10.sh +60 -0
- package/scripts/repair/supply_chain/check_11.sh +63 -0
- package/scripts/repair/supply_chain/check_12.sh +36 -0
- package/scripts/repair/supply_chain/check_13.sh +44 -0
- package/scripts/repair/supply_chain/check_14.sh +33 -0
- package/scripts/repair/supply_chain/check_15.sh +33 -0
- package/scripts/repair/supply_chain/check_16.sh +34 -0
- package/scripts/repair/supply_chain/check_17.sh +61 -0
- package/scripts/repair/supply_chain/check_18.sh +62 -0
- package/scripts/repair/supply_chain/check_2.sh +93 -0
- package/scripts/repair/supply_chain/check_3.sh +78 -0
- package/scripts/repair/supply_chain/check_4.sh +72 -0
- package/scripts/repair/supply_chain/check_5.sh +73 -0
- package/scripts/repair/supply_chain/check_6.sh +81 -0
- package/scripts/repair/supply_chain/check_7.sh +52 -0
- package/scripts/repair/supply_chain/check_8.sh +71 -0
- package/scripts/repair/supply_chain/check_9.sh +78 -0
- package/scripts/repair/supply_chain/logs/security-scan.log +77 -0
- package/scripts/scan.sh +228 -0
- package/webui/index.html +1235 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
|
|
3
|
+
export PATH="$HOME/.local/bin:/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
|
|
4
|
+
|
|
5
|
+
# ------------------------------------------------------------
|
|
6
|
+
# Shared helpers
|
|
7
|
+
# ------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
OPENCLAW_PRESENT=false
|
|
10
|
+
if command -v openclaw >/dev/null 2>&1; then
|
|
11
|
+
OPENCLAW_PRESENT=true
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
OC_VERSION="unknown"
|
|
15
|
+
PARSED_OC_VERSION=""
|
|
16
|
+
OC_MAJOR=""
|
|
17
|
+
OC_MINOR=""
|
|
18
|
+
OC_PATCH=""
|
|
19
|
+
|
|
20
|
+
if [ "$OPENCLAW_PRESENT" = true ]; then
|
|
21
|
+
OC_VERSION="$(run_with_timeout 5 openclaw --version 2>/dev/null || echo "unknown")"
|
|
22
|
+
PARSED_OC_VERSION="$(echo "$OC_VERSION" | grep -oE '[0-9]{4}\.[0-9]+\.[0-9]+' | head -1 || true)"
|
|
23
|
+
if [ -n "$PARSED_OC_VERSION" ]; then
|
|
24
|
+
OC_MAJOR="$(echo "$PARSED_OC_VERSION" | cut -d'.' -f1)"
|
|
25
|
+
OC_MINOR="$(echo "$PARSED_OC_VERSION" | cut -d'.' -f2)"
|
|
26
|
+
OC_PATCH="$(echo "$PARSED_OC_VERSION" | cut -d'.' -f3)"
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
get_oc_config() {
|
|
31
|
+
# Usage: get_oc_config <key> <default>
|
|
32
|
+
local key="$1"
|
|
33
|
+
local default_val="${2:-}"
|
|
34
|
+
if [ "$OPENCLAW_PRESENT" = true ]; then
|
|
35
|
+
run_with_timeout 10 openclaw config get "$key" 2>/dev/null || echo "$default_val"
|
|
36
|
+
else
|
|
37
|
+
echo "$default_val"
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
normalize_config_val() {
|
|
42
|
+
# Normalize null/undefined-ish values to empty string
|
|
43
|
+
local val="${1:-}"
|
|
44
|
+
case "$val" in
|
|
45
|
+
null|undefined|"")
|
|
46
|
+
echo ""
|
|
47
|
+
;;
|
|
48
|
+
*)
|
|
49
|
+
echo "$val"
|
|
50
|
+
;;
|
|
51
|
+
esac
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
version_ge() {
|
|
55
|
+
# Usage: version_ge <major> <minor> <patch>
|
|
56
|
+
# returns 0 if OC_VERSION >= target, else 1
|
|
57
|
+
local t_major="$1"
|
|
58
|
+
local t_minor="$2"
|
|
59
|
+
local t_patch="$3"
|
|
60
|
+
|
|
61
|
+
if [ -z "$PARSED_OC_VERSION" ]; then
|
|
62
|
+
return 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
if [ "$OC_MAJOR" -gt "$t_major" ] 2>/dev/null; then
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
68
|
+
if [ "$OC_MAJOR" -lt "$t_major" ] 2>/dev/null; then
|
|
69
|
+
return 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
if [ "$OC_MINOR" -gt "$t_minor" ] 2>/dev/null; then
|
|
73
|
+
return 0
|
|
74
|
+
fi
|
|
75
|
+
if [ "$OC_MINOR" -lt "$t_minor" ] 2>/dev/null; then
|
|
76
|
+
return 1
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
if [ "$OC_PATCH" -ge "$t_patch" ] 2>/dev/null; then
|
|
80
|
+
return 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
return 1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
version_lt() {
|
|
87
|
+
# Usage: version_lt <major> <minor> <patch>
|
|
88
|
+
# returns 0 if OC_VERSION < target, else 1
|
|
89
|
+
if [ -z "$PARSED_OC_VERSION" ]; then
|
|
90
|
+
return 1
|
|
91
|
+
fi
|
|
92
|
+
if version_ge "$1" "$2" "$3"; then
|
|
93
|
+
return 1
|
|
94
|
+
fi
|
|
95
|
+
return 0
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
detect_gateway_port() {
|
|
99
|
+
local raw
|
|
100
|
+
raw="$(get_oc_config "gateway.port" "18789")"
|
|
101
|
+
raw="$(echo "$raw" | grep -oE '[0-9]+' | head -1 || true)"
|
|
102
|
+
if [ -z "$raw" ]; then
|
|
103
|
+
raw="18789"
|
|
104
|
+
fi
|
|
105
|
+
echo "$raw"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
GW_PORT="$(detect_gateway_port)"
|
|
109
|
+
|
|
110
|
+
# ============================================================
|
|
111
|
+
# CHECK 1 (origin 13): Gateway Security Configuration
|
|
112
|
+
# ============================================================
|
|
113
|
+
header 1 "Auditing gateway security configuration..."
|
|
114
|
+
|
|
115
|
+
GW_ISSUES=0
|
|
116
|
+
|
|
117
|
+
if [ "$OPENCLAW_PRESENT" = true ]; then
|
|
118
|
+
log " OpenClaw version: $OC_VERSION"
|
|
119
|
+
|
|
120
|
+
BIND="$(get_oc_config "gateway.bind" "unknown")"
|
|
121
|
+
if [ "$BIND" = "lan" ] || [ "$BIND" = "0.0.0.0" ]; then
|
|
122
|
+
result_warn "Gateway bound to LAN ($BIND) - accessible from network"
|
|
123
|
+
GW_ISSUES=$((GW_ISSUES + 1))
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
AUTH_MODE="$(get_oc_config "gateway.auth.mode" "unknown")"
|
|
127
|
+
if [ "$AUTH_MODE" = "none" ] || [ "$AUTH_MODE" = "off" ]; then
|
|
128
|
+
result_critical "Gateway authentication is disabled"
|
|
129
|
+
GW_ISSUES=$((GW_ISSUES + 1))
|
|
130
|
+
fi
|
|
131
|
+
else
|
|
132
|
+
log " openclaw command not found"
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if [ "$GW_ISSUES" -eq 0 ]; then
|
|
136
|
+
result_clean "Gateway configuration acceptable"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# ============================================================
|
|
140
|
+
# CHECK 2 (origin 14): WebSocket Origin Validation
|
|
141
|
+
# ============================================================
|
|
142
|
+
header 2 "Checking WebSocket security (CVE-2026-25253)..."
|
|
143
|
+
|
|
144
|
+
if command -v curl >/dev/null 2>&1; then
|
|
145
|
+
WS_RAW="$(curl -s -o /dev/null -w "%{http_code}" \
|
|
146
|
+
-H "Connection: Upgrade" \
|
|
147
|
+
-H "Upgrade: websocket" \
|
|
148
|
+
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
|
149
|
+
-H "Sec-WebSocket-Version: 13" \
|
|
150
|
+
-H "Origin: http://evil.attacker.com" \
|
|
151
|
+
--connect-timeout 3 --max-time 5 \
|
|
152
|
+
"http://127.0.0.1:${GW_PORT}/" 2>/dev/null || echo "000")"
|
|
153
|
+
WS_TEST="$(echo "$WS_RAW" | head -c 3)"
|
|
154
|
+
else
|
|
155
|
+
WS_TEST="000"
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
if [ "$WS_TEST" = "101" ]; then
|
|
159
|
+
PATCHED_WS=false
|
|
160
|
+
if version_ge 2026 2 0; then
|
|
161
|
+
PATCHED_WS=true
|
|
162
|
+
elif version_ge 2026 1 29; then
|
|
163
|
+
PATCHED_WS=true
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
HAS_GW_AUTH="$(get_oc_config "gateway.auth.mode" "")"
|
|
167
|
+
if [ "$PATCHED_WS" = true ] && [ -n "$HAS_GW_AUTH" ] && [ "$HAS_GW_AUTH" != "none" ] && [ "$HAS_GW_AUTH" != "off" ]; then
|
|
168
|
+
result_clean "WebSocket: v$OC_VERSION patched + gateway auth ($HAS_GW_AUTH) - CVE-2026-25253 mitigated"
|
|
169
|
+
elif [ "$PATCHED_WS" = true ]; then
|
|
170
|
+
result_warn "WebSocket: v$OC_VERSION patched but gateway auth is weak/missing"
|
|
171
|
+
else
|
|
172
|
+
result_critical "Gateway accepts WebSocket from arbitrary origins (CVE-2026-25253 may be unpatched)"
|
|
173
|
+
fi
|
|
174
|
+
elif [ "$WS_TEST" = "000" ]; then
|
|
175
|
+
log " Gateway not reachable on port $GW_PORT (may be normal)"
|
|
176
|
+
result_clean "WebSocket test inconclusive (gateway not reachable)"
|
|
177
|
+
elif [ "$WS_TEST" = "403" ] || [ "$WS_TEST" = "401" ]; then
|
|
178
|
+
result_clean "WebSocket origin validation active (HTTP $WS_TEST rejected)"
|
|
179
|
+
else
|
|
180
|
+
result_clean "WebSocket responded HTTP $WS_TEST"
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# ============================================================
|
|
184
|
+
# CHECK 3 (origin 25): Reverse Proxy / Localhost Trust Bypass
|
|
185
|
+
# ============================================================
|
|
186
|
+
header 3 "Checking for reverse proxy localhost trust bypass..."
|
|
187
|
+
|
|
188
|
+
PROXY_ISSUES=0
|
|
189
|
+
|
|
190
|
+
if [ "$OPENCLAW_PRESENT" = true ]; then
|
|
191
|
+
BIND_ADDR="$(get_oc_config "gateway.bind" "")"
|
|
192
|
+
TRUSTED_PROXIES="$(get_oc_config "gateway.trustedProxies" "")"
|
|
193
|
+
DISABLE_DEVICE_AUTH="$(get_oc_config "gateway.dangerouslyDisableDeviceAuth" "")"
|
|
194
|
+
|
|
195
|
+
if [ "$DISABLE_DEVICE_AUTH" = "true" ]; then
|
|
196
|
+
result_critical "Device authentication is disabled (dangerouslyDisableDeviceAuth=true)"
|
|
197
|
+
PROXY_ISSUES=$((PROXY_ISSUES + 1))
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
if [ "$BIND_ADDR" = "lan" ] || [ "$BIND_ADDR" = "0.0.0.0" ]; then
|
|
201
|
+
if [ -z "$TRUSTED_PROXIES" ] || [ "$TRUSTED_PROXIES" = "null" ] || [ "$TRUSTED_PROXIES" = "[]" ]; then
|
|
202
|
+
result_warn "Gateway on LAN without trustedProxies - localhost trust bypass risk"
|
|
203
|
+
PROXY_ISSUES=$((PROXY_ISSUES + 1))
|
|
204
|
+
fi
|
|
205
|
+
fi
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
if [ "$PROXY_ISSUES" -eq 0 ]; then
|
|
209
|
+
result_clean "No reverse proxy bypass risk"
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
# ============================================================
|
|
213
|
+
# CHECK 4 (origin 33): ClawJacked Brute-Force Protection
|
|
214
|
+
# ============================================================
|
|
215
|
+
header 4 "Checking ClawJacked brute-force protection..."
|
|
216
|
+
|
|
217
|
+
CLAWJACKED_ISSUES=0
|
|
218
|
+
|
|
219
|
+
if [ "$OPENCLAW_PRESENT" = true ]; then
|
|
220
|
+
if [ -n "$PARSED_OC_VERSION" ]; then
|
|
221
|
+
CLAWJACKED_SAFE=false
|
|
222
|
+
if version_ge 2026 3 0; then
|
|
223
|
+
CLAWJACKED_SAFE=true
|
|
224
|
+
elif version_ge 2026 2 26; then
|
|
225
|
+
CLAWJACKED_SAFE=true
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
if [ "$CLAWJACKED_SAFE" = false ]; then
|
|
229
|
+
result_critical "OpenClaw v$OC_VERSION vulnerable to ClawJacked (WebSocket brute-force). Update to v2026.2.26+"
|
|
230
|
+
CLAWJACKED_ISSUES=$((CLAWJACKED_ISSUES + 1))
|
|
231
|
+
fi
|
|
232
|
+
else
|
|
233
|
+
result_warn "Could not parse OpenClaw version from: $OC_VERSION"
|
|
234
|
+
CLAWJACKED_ISSUES=$((CLAWJACKED_ISSUES + 1))
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
GW_AUTH_TOKEN="$(get_oc_config "gateway.auth.token" "")"
|
|
238
|
+
if [ -z "$GW_AUTH_TOKEN" ] || [ "$GW_AUTH_TOKEN" = "null" ]; then
|
|
239
|
+
result_warn "No gateway auth token set (ClawJacked can brute-force weak/default credentials more easily)"
|
|
240
|
+
CLAWJACKED_ISSUES=$((CLAWJACKED_ISSUES + 1))
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
RATE_LIMIT="$(get_oc_config "gateway.auth.rateLimit" "")"
|
|
244
|
+
if [ "$RATE_LIMIT" = "off" ] || [ "$RATE_LIMIT" = "false" ]; then
|
|
245
|
+
result_warn "Gateway auth rate limiting is disabled (enables brute-force attacks)"
|
|
246
|
+
CLAWJACKED_ISSUES=$((CLAWJACKED_ISSUES + 1))
|
|
247
|
+
fi
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
if [ "$CLAWJACKED_ISSUES" -eq 0 ]; then
|
|
251
|
+
result_clean "ClawJacked protections in place"
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
# ============================================================
|
|
255
|
+
# CHECK 5 (origin 41): Browser Relay CDP Unauthenticated Access
|
|
256
|
+
# ============================================================
|
|
257
|
+
header 5 "Checking Browser Relay CDP auth (CVE-2026-28458)..."
|
|
258
|
+
|
|
259
|
+
CDP_ISSUES=0
|
|
260
|
+
|
|
261
|
+
if command -v lsof >/dev/null 2>&1; then
|
|
262
|
+
CDP_LISTEN="$(lsof -iTCP:18792 -sTCP:LISTEN -nP 2>/dev/null | grep -v '^COMMAND' || true)"
|
|
263
|
+
if [ -n "$CDP_LISTEN" ]; then
|
|
264
|
+
log " Browser Relay is listening on port 18792"
|
|
265
|
+
if [ -n "$PARSED_OC_VERSION" ]; then
|
|
266
|
+
if version_lt 2026 2 1; then
|
|
267
|
+
result_critical "Browser Relay /cdp endpoint may be unauthenticated (CVE-2026-28458). Update to v2026.2.1+"
|
|
268
|
+
CDP_ISSUES=$((CDP_ISSUES + 1))
|
|
269
|
+
fi
|
|
270
|
+
fi
|
|
271
|
+
fi
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
if [ "$CDP_ISSUES" -eq 0 ]; then
|
|
275
|
+
result_clean "Browser Relay CDP auth acceptable"
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
# ============================================================
|
|
279
|
+
# CHECK 6 (origin 31): Internet Exposure Detection
|
|
280
|
+
# ============================================================
|
|
281
|
+
header 6 "Checking for internet exposure of gateway..."
|
|
282
|
+
|
|
283
|
+
EXPOSURE_ISSUES=0
|
|
284
|
+
|
|
285
|
+
if command -v lsof >/dev/null 2>&1; then
|
|
286
|
+
GW_LISTEN="$(lsof -i ":${GW_PORT}" -nP 2>/dev/null | grep LISTEN | awk '{print $9}' | head -5 || true)"
|
|
287
|
+
if [ -n "$GW_LISTEN" ]; then
|
|
288
|
+
NON_LOCAL="$(echo "$GW_LISTEN" | grep -vE '127\.0\.0\.1|localhost|\[::1\]' || true)"
|
|
289
|
+
if [ -n "$NON_LOCAL" ]; then
|
|
290
|
+
result_warn "Gateway listening on non-loopback interface"
|
|
291
|
+
log "$GW_LISTEN"
|
|
292
|
+
EXPOSURE_ISSUES=$((EXPOSURE_ISSUES + 1))
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
WILDCARD="$(echo "$GW_LISTEN" | grep '\*:' || true)"
|
|
296
|
+
if [ -n "$WILDCARD" ]; then
|
|
297
|
+
result_warn "Gateway bound to all interfaces (*:$GW_PORT) - potentially internet-exposed"
|
|
298
|
+
EXPOSURE_ISSUES=$((EXPOSURE_ISSUES + 1))
|
|
299
|
+
fi
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
if [ "$EXPOSURE_ISSUES" -eq 0 ]; then
|
|
304
|
+
result_clean "Gateway not exposed to external network"
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
# ============================================================
|
|
308
|
+
# CHECK 7 (origin 20): mDNS/Bonjour Exposure
|
|
309
|
+
# ============================================================
|
|
310
|
+
header 7 "Checking mDNS/Bonjour discovery settings..."
|
|
311
|
+
|
|
312
|
+
MDNS_ISSUES=0
|
|
313
|
+
|
|
314
|
+
if [ "$OPENCLAW_PRESENT" = true ]; then
|
|
315
|
+
MDNS_MODE="$(get_oc_config "discovery.mdns.mode" "")"
|
|
316
|
+
if [ "$MDNS_MODE" = "full" ]; then
|
|
317
|
+
result_warn "mDNS broadcasting in 'full' mode (exposes paths, SSH port)"
|
|
318
|
+
MDNS_ISSUES=$((MDNS_ISSUES + 1))
|
|
319
|
+
elif [ -n "$MDNS_MODE" ]; then
|
|
320
|
+
log " mDNS mode: $MDNS_MODE"
|
|
321
|
+
fi
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
if [ "$MDNS_ISSUES" -eq 0 ]; then
|
|
325
|
+
result_clean "mDNS configuration acceptable"
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
# ============================================================
|
|
329
|
+
# CHECK 8 (origin 34): SSRF Protection
|
|
330
|
+
# ============================================================
|
|
331
|
+
header 8 "Checking SSRF protections (CVE-2026-26322, CVE-2026-27488)..."
|
|
332
|
+
|
|
333
|
+
SSRF_ISSUES=0
|
|
334
|
+
|
|
335
|
+
if [ "$OPENCLAW_PRESENT" = true ]; then
|
|
336
|
+
if [ -n "$PARSED_OC_VERSION" ]; then
|
|
337
|
+
if version_lt 2026 2 14; then
|
|
338
|
+
result_critical "OpenClaw v$OC_VERSION vulnerable to gateway SSRF (CVE-2026-26322) and cron SSRF (CVE-2026-27488). Update to v2026.2.19+"
|
|
339
|
+
SSRF_ISSUES=$((SSRF_ISSUES + 1))
|
|
340
|
+
elif version_lt 2026 2 19; then
|
|
341
|
+
result_warn "OpenClaw v$OC_VERSION vulnerable to cron webhook SSRF (CVE-2026-27488). Update to v2026.2.19+"
|
|
342
|
+
SSRF_ISSUES=$((SSRF_ISSUES + 1))
|
|
343
|
+
fi
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
CRON_CONFIG="$(get_oc_config "cron" "")"
|
|
347
|
+
if echo "$CRON_CONFIG" | grep -qiE '169\.254\.|127\.0\.0\.1|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|metadata\.google|metadata\.aws'; then
|
|
348
|
+
result_warn "Cron webhook targets internal/metadata endpoints (SSRF risk)"
|
|
349
|
+
SSRF_ISSUES=$((SSRF_ISSUES + 1))
|
|
350
|
+
fi
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
if [ "$SSRF_ISSUES" -eq 0 ]; then
|
|
354
|
+
result_clean "SSRF protections acceptable"
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
# ============================================================
|
|
358
|
+
# CHECK 9 (origin 36): ACP Permission Auto-Approval
|
|
359
|
+
# ============================================================
|
|
360
|
+
header 9 "Checking ACP auto-approval bypass (GHSA-7jx5)..."
|
|
361
|
+
|
|
362
|
+
ACP_ISSUES=0
|
|
363
|
+
|
|
364
|
+
if [ "$OPENCLAW_PRESENT" = true ]; then
|
|
365
|
+
if [ -n "$PARSED_OC_VERSION" ]; then
|
|
366
|
+
if version_lt 2026 2 23; then
|
|
367
|
+
result_warn "OpenClaw v$OC_VERSION vulnerable to GHSA-7jx5 (ACP auto-approval bypass). Update to v2026.2.23+"
|
|
368
|
+
ACP_ISSUES=$((ACP_ISSUES + 1))
|
|
369
|
+
fi
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
ACP_AUTO="$(get_oc_config "acp.autoApprove" "")"
|
|
373
|
+
if [ "$ACP_AUTO" = "true" ] || [ "$ACP_AUTO" = "all" ]; then
|
|
374
|
+
result_warn "ACP auto-approve is enabled (tool calls bypass interactive approval)"
|
|
375
|
+
ACP_ISSUES=$((ACP_ISSUES + 1))
|
|
376
|
+
fi
|
|
377
|
+
fi
|
|
378
|
+
|
|
379
|
+
if [ "$ACP_ISSUES" -eq 0 ]; then
|
|
380
|
+
result_clean "ACP permission controls acceptable"
|
|
381
|
+
fi
|
|
382
|
+
|
|
383
|
+
# ============================================================
|
|
384
|
+
# CHECK 10 (origin 39/45): Deep Link + Sandbox Browser Bridge
|
|
385
|
+
# ============================================================
|
|
386
|
+
header 10 "Checking deep link truncation and sandbox browser bridge auth..."
|
|
387
|
+
|
|
388
|
+
LOCAL_BRIDGE_ISSUES=0
|
|
389
|
+
|
|
390
|
+
# ---- Deep link truncation (origin 39)
|
|
391
|
+
if [ "$(uname -s)" = "Darwin" ]; then
|
|
392
|
+
if [ -n "$PARSED_OC_VERSION" ] && version_lt 2026 2 14; then
|
|
393
|
+
result_warn "OpenClaw v$OC_VERSION vulnerable to deep link truncation (CVE-2026-26320). Update to v2026.2.14+"
|
|
394
|
+
LOCAL_BRIDGE_ISSUES=$((LOCAL_BRIDGE_ISSUES + 1))
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
if [ -d "$LOG_DIR" ]; then
|
|
398
|
+
LONG_LINKS="$(grep -rl 'openclaw://' "$LOG_DIR" 2>/dev/null | head -3 || true)"
|
|
399
|
+
if [ -n "$LONG_LINKS" ]; then
|
|
400
|
+
while IFS= read -r lfile; do
|
|
401
|
+
[ -z "$lfile" ] && continue
|
|
402
|
+
if grep -E 'openclaw://[^ ]{240,}' "$lfile" >/dev/null 2>&1; then
|
|
403
|
+
result_warn "Long deep link (>240 chars) found in logs - possible CVE-2026-26320 exploit attempt"
|
|
404
|
+
LOCAL_BRIDGE_ISSUES=$((LOCAL_BRIDGE_ISSUES + 1))
|
|
405
|
+
break
|
|
406
|
+
fi
|
|
407
|
+
done <<EOF
|
|
408
|
+
$LONG_LINKS
|
|
409
|
+
EOF
|
|
410
|
+
fi
|
|
411
|
+
fi
|
|
412
|
+
else
|
|
413
|
+
log " Not macOS, skipping deep-link truncation check"
|
|
414
|
+
fi
|
|
415
|
+
|
|
416
|
+
# ---- Sandbox browser bridge auth bypass (origin 45)
|
|
417
|
+
if [ -n "$PARSED_OC_VERSION" ] && version_lt 2026 2 14; then
|
|
418
|
+
result_warn "Sandbox browser bridge may be unauthenticated (CVE-2026-28468). Update to v2026.2.14+"
|
|
419
|
+
LOCAL_BRIDGE_ISSUES=$((LOCAL_BRIDGE_ISSUES + 1))
|
|
420
|
+
fi
|
|
421
|
+
|
|
422
|
+
if [ "$LOCAL_BRIDGE_ISSUES" -eq 0 ]; then
|
|
423
|
+
result_clean "Deep link and sandbox browser bridge protections acceptable"
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
# ============================================================
|
|
427
|
+
# CHECK 11 (new / GW-003): Weak gateway auth token/password
|
|
428
|
+
# ============================================================
|
|
429
|
+
header 11 "Checking gateway auth token/password strength..."
|
|
430
|
+
|
|
431
|
+
GW_WEAK_AUTH_ISSUES=0
|
|
432
|
+
|
|
433
|
+
if [ "$OPENCLAW_PRESENT" = true ]; then
|
|
434
|
+
AUTH_MODE="$(normalize_config_val "$(get_oc_config "gateway.auth.mode" "")")"
|
|
435
|
+
|
|
436
|
+
GW_AUTH_TOKEN_CANDIDATE="$(normalize_config_val "$(get_oc_config "gateway.auth.token" "")")"
|
|
437
|
+
GW_AUTH_PASSWORD_CANDIDATE="$(normalize_config_val "$(get_oc_config "gateway.auth.password" "")")"
|
|
438
|
+
GW_AUTH_LEGACY_CANDIDATE="$(normalize_config_val "$(get_oc_config "gateway.authToken" "")")"
|
|
439
|
+
|
|
440
|
+
GW_AUTH_SECRET=""
|
|
441
|
+
if [ -n "$GW_AUTH_TOKEN_CANDIDATE" ]; then
|
|
442
|
+
GW_AUTH_SECRET="$GW_AUTH_TOKEN_CANDIDATE"
|
|
443
|
+
elif [ -n "$GW_AUTH_PASSWORD_CANDIDATE" ]; then
|
|
444
|
+
GW_AUTH_SECRET="$GW_AUTH_PASSWORD_CANDIDATE"
|
|
445
|
+
elif [ -n "$GW_AUTH_LEGACY_CANDIDATE" ]; then
|
|
446
|
+
GW_AUTH_SECRET="$GW_AUTH_LEGACY_CANDIDATE"
|
|
447
|
+
fi
|
|
448
|
+
|
|
449
|
+
if [ "$AUTH_MODE" = "token" ] || [ "$AUTH_MODE" = "password" ]; then
|
|
450
|
+
GW_SECRET_LEN="$(printf "%s" "$GW_AUTH_SECRET" | wc -c | tr -d ' ' || echo "0")"
|
|
451
|
+
if [ "$GW_SECRET_LEN" -gt 0 ] 2>/dev/null && [ "$GW_SECRET_LEN" -lt 32 ] 2>/dev/null; then
|
|
452
|
+
result_warn "Weak gateway authentication token/password (${GW_SECRET_LEN} chars). Minimum 32 recommended"
|
|
453
|
+
GW_WEAK_AUTH_ISSUES=$((GW_WEAK_AUTH_ISSUES + 1))
|
|
454
|
+
fi
|
|
455
|
+
fi
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
if [ "$GW_WEAK_AUTH_ISSUES" -eq 0 ]; then
|
|
459
|
+
result_clean "Gateway auth token/password length acceptable"
|
|
460
|
+
fi
|
|
461
|
+
|
|
462
|
+
# ============================================================
|
|
463
|
+
# CHECK 12 (new / GW-006): Gateway TLS enabled
|
|
464
|
+
# ============================================================
|
|
465
|
+
header 12 "Checking whether gateway TLS is enabled..."
|
|
466
|
+
|
|
467
|
+
GW_TLS_ISSUES=0
|
|
468
|
+
|
|
469
|
+
if [ "$OPENCLAW_PRESENT" = true ]; then
|
|
470
|
+
GW_TLS_ENABLED="$(normalize_config_val "$(get_oc_config "gateway.tls.enabled" "")")"
|
|
471
|
+
if [ "$GW_TLS_ENABLED" != "true" ]; then
|
|
472
|
+
result_warn "TLS not enabled on gateway (gateway.tls.enabled is not true) - credentials and traffic may be sent in plaintext"
|
|
473
|
+
GW_TLS_ISSUES=$((GW_TLS_ISSUES + 1))
|
|
474
|
+
fi
|
|
475
|
+
fi
|
|
476
|
+
|
|
477
|
+
if [ "$GW_TLS_ISSUES" -eq 0 ]; then
|
|
478
|
+
result_clean "Gateway TLS is enabled"
|
|
479
|
+
fi
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
|
|
3
|
+
# ------------------------------------------------------------
|
|
4
|
+
# Shared helpers
|
|
5
|
+
# ------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
OPENCLAW_PRESENT=false
|
|
8
|
+
if command -v openclaw >/dev/null 2>&1; then
|
|
9
|
+
OPENCLAW_PRESENT=true
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
OC_VERSION="unknown"
|
|
13
|
+
PARSED_OC_VERSION=""
|
|
14
|
+
OC_MAJOR=""
|
|
15
|
+
OC_MINOR=""
|
|
16
|
+
OC_PATCH=""
|
|
17
|
+
|
|
18
|
+
if [ "$OPENCLAW_PRESENT" = true ]; then
|
|
19
|
+
OC_VERSION="$(run_with_timeout 5 openclaw --version 2>/dev/null || echo "unknown")"
|
|
20
|
+
PARSED_OC_VERSION="$(echo "$OC_VERSION" | grep -oE '[0-9]{4}\.[0-9]+\.[0-9]+' | head -1 || true)"
|
|
21
|
+
|
|
22
|
+
if [ -n "$PARSED_OC_VERSION" ]; then
|
|
23
|
+
OC_MAJOR="$(echo "$PARSED_OC_VERSION" | cut -d'.' -f1)"
|
|
24
|
+
OC_MINOR="$(echo "$PARSED_OC_VERSION" | cut -d'.' -f2)"
|
|
25
|
+
OC_PATCH="$(echo "$PARSED_OC_VERSION" | cut -d'.' -f3)"
|
|
26
|
+
fi
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
version_ge() {
|
|
30
|
+
|
|
31
|
+
local t_major="$1"
|
|
32
|
+
local t_minor="$2"
|
|
33
|
+
local t_patch="$3"
|
|
34
|
+
|
|
35
|
+
if [ -z "$PARSED_OC_VERSION" ]; then
|
|
36
|
+
return 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [ "$OC_MAJOR" -gt "$t_major" ] 2>/dev/null; then
|
|
40
|
+
return 0
|
|
41
|
+
fi
|
|
42
|
+
if [ "$OC_MAJOR" -lt "$t_major" ] 2>/dev/null; then
|
|
43
|
+
return 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [ "$OC_MINOR" -gt "$t_minor" ] 2>/dev/null; then
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
if [ "$OC_MINOR" -lt "$t_minor" ] 2>/dev/null; then
|
|
50
|
+
return 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if [ "$OC_PATCH" -ge "$t_patch" ] 2>/dev/null; then
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
return 1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
version_lt() {
|
|
61
|
+
|
|
62
|
+
if [ -z "$PARSED_OC_VERSION" ]; then
|
|
63
|
+
return 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
if version_ge "$1" "$2" "$3"; then
|
|
67
|
+
return 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
return 0
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------
|
|
74
|
+
# Cron frequency check
|
|
75
|
+
# ------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
has_high_freq_cron() {
|
|
78
|
+
|
|
79
|
+
grep -Eq '^[[:space:]]*(\*[[:space:]]+\*[[:space:]]+\*[[:space:]]+\*[[:space:]]+\*|\*/[1-4][[:space:]]+\*[[:space:]]+\*[[:space:]]+\*[[:space:]]+\*)' "$1" 2>/dev/null
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
TOTAL_TOKENS=0
|
|
84
|
+
TOTAL_COST=0
|
|
85
|
+
HOURLY_COST=0
|
|
86
|
+
|
|
87
|
+
# ============================================================
|
|
88
|
+
# CHECK 1: Webhook DoS
|
|
89
|
+
# ============================================================
|
|
90
|
+
|
|
91
|
+
header 1 "Checking webhook DoS / oversized payloads (CVE-2026-28478)..."
|
|
92
|
+
|
|
93
|
+
WDOS_ISSUES=0
|
|
94
|
+
|
|
95
|
+
if [ "$OPENCLAW_PRESENT" = true ]; then
|
|
96
|
+
|
|
97
|
+
if version_lt 2026 2 13; then
|
|
98
|
+
|
|
99
|
+
result_warn "Webhook handlers may lack strict body size/time limits (CVE-2026-28478). Update to v2026.2.13+"
|
|
100
|
+
|
|
101
|
+
WDOS_ISSUES=$((WDOS_ISSUES+1))
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
else
|
|
105
|
+
|
|
106
|
+
log "openclaw command not found"
|
|
107
|
+
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
if [ "$WDOS_ISSUES" -eq 0 ]; then
|
|
111
|
+
result_clean "Webhook body limits acceptable"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ============================================================
|
|
116
|
+
# CHECK 2: fetchWithGuard DoS
|
|
117
|
+
# ============================================================
|
|
118
|
+
|
|
119
|
+
header 2 "Checking fetchWithGuard memory DoS (CVE-2026-29609)..."
|
|
120
|
+
|
|
121
|
+
FWG_ISSUES=0
|
|
122
|
+
|
|
123
|
+
if [ "$OPENCLAW_PRESENT" = true ]; then
|
|
124
|
+
|
|
125
|
+
if version_lt 2026 2 14; then
|
|
126
|
+
|
|
127
|
+
result_warn "fetchWithGuard may be vulnerable to memory exhaustion DoS (CVE-2026-29609). Update to v2026.2.14+"
|
|
128
|
+
|
|
129
|
+
FWG_ISSUES=$((FWG_ISSUES+1))
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
else
|
|
133
|
+
|
|
134
|
+
log "openclaw command not found"
|
|
135
|
+
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
if [ "$FWG_ISSUES" -eq 0 ]; then
|
|
139
|
+
result_clean "fetchWithGuard memory handling acceptable"
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ============================================================
|
|
144
|
+
# CHECK 3: High frequency cron
|
|
145
|
+
# ============================================================
|
|
146
|
+
|
|
147
|
+
header 3 "Checking high-frequency scheduled agent invocations (> every 5 minutes)..."
|
|
148
|
+
|
|
149
|
+
CRON_ISSUES=0
|
|
150
|
+
|
|
151
|
+
LOCAL_CRON_FILE="$OPENCLAW_DIR/crontab"
|
|
152
|
+
|
|
153
|
+
if [ -f "$LOCAL_CRON_FILE" ]; then
|
|
154
|
+
|
|
155
|
+
if has_high_freq_cron "$LOCAL_CRON_FILE"; then
|
|
156
|
+
|
|
157
|
+
result_warn "High-frequency schedules found in $LOCAL_CRON_FILE (more often than every 5 minutes)"
|
|
158
|
+
|
|
159
|
+
CRON_ISSUES=$((CRON_ISSUES+1))
|
|
160
|
+
fi
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
if command -v crontab >/dev/null 2>&1; then
|
|
164
|
+
|
|
165
|
+
TMP_CRON=$(mktemp)
|
|
166
|
+
|
|
167
|
+
if crontab -l >"$TMP_CRON" 2>/dev/null; then
|
|
168
|
+
|
|
169
|
+
if has_high_freq_cron "$TMP_CRON"; then
|
|
170
|
+
|
|
171
|
+
result_warn "High-frequency schedules found in user crontab (more often than every 5 minutes)"
|
|
172
|
+
|
|
173
|
+
CRON_ISSUES=$((CRON_ISSUES+1))
|
|
174
|
+
fi
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
rm -f "$TMP_CRON"
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
if [ "$CRON_ISSUES" -eq 0 ]; then
|
|
181
|
+
result_clean "No high-frequency scheduled invocations detected"
|
|
182
|
+
fi
|