-
Notifications
You must be signed in to change notification settings - Fork 14
Description
π Executive Summary
This review analyzed the gh-aw-firewall codebase across all security layers: network filtering (iptables/Squid), container hardening (capabilities, seccomp, privilege drop), domain validation, input handling, and dependency security. No active "Firewall Escape Test Agent" workflow was found β security review is based entirely on static code analysis and evidence gathered by running commands against the codebase.
Security posture: Good with notable gaps. The defense-in-depth architecture is sound (iptables + Squid + capability dropping + one-shot tokens), but several concrete weaknesses were identified. Two are high severity: the permissive custom seccomp profile and the IPv6 TCP bypass at the container level.
| Category | Findings | Critical | High | Medium | Low |
|---|---|---|---|---|---|
| Network filtering | 3 | 1 | 1 | 1 | 0 |
| Container hardening | 2 | 0 | 1 | 1 | 0 |
| Domain validation | 1 | 0 | 1 | 0 | 0 |
| Token security | 2 | 0 | 0 | 2 | 0 |
| Dependencies | 2 | 0 | 1 | 1 | 0 |
| Input handling | 2 | 0 | 0 | 1 | 1 |
π Findings from Complementary Security Workflows
The workflow named "Firewall Escape Test Agent" was not found in the available workflow list (agenticworkflows-status). Other security-adjacent workflows exist β secret-digger-claude, secret-digger-codex, secret-digger-copilot, and security-guard β but their recent results were unavailable due to authentication constraints. This review is therefore independent of any prior escape-test results.
π‘οΈ Architecture Security Analysis
Network Security Assessment
Design: The firewall uses a two-layer model:
- Host-level (
src/host-iptables.ts):DOCKER-USERchain filtering viaFW_WRAPPERcustom chain - Container-level (
containers/agent/setup-iptables.sh): NAT-table DNAT rules redirecting HTTP/HTTPS to Squid
Strengths observed:
- DNS exfiltration prevention: Only trusted servers (default: 8.8.8.8, 8.8.4.4) are whitelisted; all other UDP is rejected (
[FW_BLOCKED_UDP]logged, thenREJECT) - Dangerous port blocklist (
src/squid-config.ts+setup-iptables.sh): 15 ports (22, 25, 3306, 5432, 6379β¦) blocked at both layers - Default-deny for TCP at container level:
iptables -A OUTPUT -p tcp -j DROP(line 293 of setup-iptables.sh) - Squid ACL deny-before-allow ordering (confirmed in
src/squid-config.ts:388-396)
Finding 1 β CRITICAL: IPv6 TCP bypasses Squid proxy at container level
Evidence:
$ grep -n "ip6tables.*tcp.*DROP\|ip6tables.*OUTPUT.*tcp" containers/agent/setup-iptables.sh
# (no results)setup-iptables.sh configures iptables -A OUTPUT -p tcp -j DROP for IPv4, but there is no ip6tables -A OUTPUT -p tcp -j DROP. IPv6 TCP traffic (to ports 80, 443, or any other) is never DNAT'd to Squid because the DNAT rules only apply to iptables (IPv4 NAT), not ip6tables. If the container has IPv6 connectivity, an agent can make direct HTTP/HTTPS connections to any IPv6-addressed host without passing through Squid domain filtering.
Additionally, the host-level IPv6 chain (FW_WRAPPER_V6) is only created when IPv6 DNS servers are configured (src/host-iptables.ts:313: if (ipv6DnsServers.length > 0)). With the default configuration (IPv4 DNS only β 8.8.8.8,8.8.4.4), the IPv6 DOCKER-USER chain is never set up, leaving IPv6 traffic entirely unfiltered at the host level too.
Finding 2 β HIGH: UDP non-DNS traffic unfiltered at container level
Evidence:
$ grep -n "udp" containers/agent/setup-iptables.sh | grep -v "echo\|#\|53"
# no matches for non-DNS UDP rulessetup-iptables.sh adds iptables -A OUTPUT -p tcp -j DROP but has no iptables -A OUTPUT -p udp -j DROP for non-DNS UDP. NTP (port 123), QUIC (UDP 443), and other UDP exfiltration vectors are not blocked at the container layer. The host-level FW_WRAPPER chain does block UDP via [FW_BLOCKED_UDP] β REJECT, but this is a single-layer defence.
Finding 3 β MEDIUM: IPv6 host filtering only activates with IPv6 DNS servers
Evidence: src/host-iptables.ts:313:
if (ipv6DnsServers.length > 0) {
const ip6tablesAvailable = await isIp6tablesAvailable();
if (!ip6tablesAvailable) { ... } else {
await setupIpv6Chain(bridgeName); // Only called hereDefault usage (--dns-servers not specified β ['8.8.8.8', '8.8.4.4']) never creates FW_WRAPPER_V6. IPv6 container egress is therefore unfiltered at the host level in all typical deployments unless the user explicitly provides an IPv6 DNS server.
Container Security Assessment
Strengths observed:
NET_ADMINadded for setup, then explicitly dropped viacapsh --drop=cap_net_adminbefore user code runs (entrypoint.sh lines 639, 648)NET_RAWdropped at Docker Compose level preventing raw socket bypassno-new-privilegessecurity option set (docker-manager.ts:879)- UID/GID remapping to match host user with validation (cannot be 0/root)
- One-shot token library clears sensitive env vars from
/proc/self/environ
Finding 4 β HIGH: Custom seccomp profile is significantly more permissive than Docker default
Evidence:
$ cat containers/agent/seccomp-profile.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['defaultAction'])"
SCMP_ACT_ALLOW
```
The custom profile (`containers/agent/seccomp-profile.json`) uses `defaultAction: SCMP_ACT_ALLOW` with only 3 deny groups (28 syscalls total). Docker's default seccomp profile denies approximately 300+ syscalls. The following dangerous syscalls are **ALLOWED** by the custom profile but blocked by Docker's default:
```
bpf β can read kernel data structures, create eBPF programs
perf_event_open β kernel performance monitoring, can leak kernel memory
userfaultfd β user-space fault handling (used in sandbox escapes)
io_uring_setup β async I/O, has had multiple kernel CVEs
clone / clone3 β namespace/process creation
seccomp β can modify own seccomp rulesThe custom profile was likely created to allow mount (for procfs in chroot mode) and umount2 needed by the agent container. However, it simultaneously removes the protection of Docker's comprehensive default deny list. A more targeted approach would be to start with Docker's default profile and add only the specific exceptions needed.
Finding 5 β MEDIUM: 5-second token exposure window
Evidence:
$ grep -n "sleep 5\|unset_sensitive\|AGENT_PID" containers/agent/entrypoint.sh
287: unset_sensitive_tokens() {
611: sleep 5 β explicit wait
614: unset_sensitive_tokens
617: wait $AGENT_PIDTokens are visible in /proc/1/environ (the entrypoint shell's environment) for 5 seconds after the agent process starts. The sleep 5 is intentional β to give the one-shot-token.so library time to cache tokens before they're unset β but creates a window where another process inside the container (or processes reading /proc) could read tokens.
Domain Validation Assessment
Strengths observed:
*and*.*patterns blocked*.*.com(multiple wildcard segments) blocked- Double-dots blocked
- Patterns that are purely wildcards/dots blocked
- Max domain length check (512 chars) for ReDoS protection
DOMAIN_CHAR_PATTERN = '[a-zA-Z0-9.-]*'instead of.*prevents ReDoS in wildcard conversion
Finding 6 β HIGH: Single-wildcard TLD patterns allowed (e.g., *.com)
Evidence:
$ node -e "
const { validateDomainOrPattern } = require('./dist/domain-patterns.js');
try { validateDomainOrPattern('*.com'); console.log('*.com: ALLOWED'); } catch(e) { console.log('*.com: BLOCKED'); }
try { validateDomainOrPattern('*.net'); console.log('*.net: ALLOWED'); } catch(e) { console.log('*.net: BLOCKED'); }
"
*.com: ALLOWED
*.net: ALLOWEDThe validation in src/domain-patterns.ts blocks patterns where wildcardSegments > 1 && wildcardSegments >= totalSegments - 1, but *.com has only 1 wildcard and 2 segments, failing the > 1 condition. This allows an operator to pass --allow-domains *.com which grants access to every .com domain β millions of destinations. While this is user-controlled, it undermines the intent of domain allowlisting and may be surprising to users who think the validator enforces specificity.
Input Validation Assessment
Strengths observed:
escapeShellArg()function insrc/cli.ts(lines 496-509) properly wraps args in single quotes and escapes inner single quotes- DNS server IP format validation in CLI
- Volume mount validation
- UID/GID numeric validation with root (0) rejection
Finding 7 β MEDIUM: AWF_HOST_PATH written to shell script without sanitization
Evidence (docker-manager.ts:396):
environment.AWF_HOST_PATH = process.env.PATH;Then in containers/agent/entrypoint.sh (chroot mode):
echo "export PATH=\"\$\{AWF_HOST_PATH}\"" >> "/host\$\{SCRIPT_FILE}"If the host PATH contains characters like ", \, $(...), or backticks, they would be interpolated when this heredoc/echo line is executed during chroot setup. In practice, PATH rarely contains such characters, but it represents an untrusted-input injection path from environment into an executed shell script. An attacker who can influence the runner's PATH (e.g., via a CI workflow) could inject commands.
Finding 8 β LOW: Predictable temp script filename in chroot mode
Evidence (containers/agent/entrypoint.sh):
SCRIPT_FILE="/tmp/awf-cmd-$$.sh"
```
The `$$` PID is predictable from within the container. Another process running concurrently in the container could race to write to the script file before it is created, or read it to discover the command being executed. Risk is mitigated by the fact that user code only runs after the script is created and executed.
---
### Dependency Security Assessment
**Finding 9 β HIGH: `minimatch` ReDoS vulnerability**
Evidence:
```
$ npm audit
minimatch 10.0.0 - 10.2.2
Severity: high
minimatch has ReDoS: matchOne() combinatorial backtracking via multiple non-adjacent GLOBSTAR segments
minimatch ReDoS: nested *() extglobs generate catastrophically backtracking regular expressions
fix available via `npm audit fix`
```
`minimatch` is used internally in the build toolchain. While not directly user-facing in the firewall's runtime path, it represents an unaddressed high-severity supply chain concern.
**Finding 10 β MODERATE: `ajv` ReDoS vulnerability**
Evidence:
```
ajv <6.14.0 || >=7.0.0-alpha.0 <8.18.0
Severity: moderate
ajv has ReDoS when using `$data` option
fix available via `npm audit fix`β οΈ Threat Model (STRIDE)
| Threat | Vector | Likelihood | Impact | Evidence |
|---|---|---|---|---|
| Spoofing β DNS spoofing to bypass domain ACL | DNS response spoofing if using untrusted network path to 8.8.8.8 | Low | High | DNS traffic to 8.8.8.8 via plaintext UDP; no DNSSEC enforcement |
| Tampering β iptables modification | Agent exploits allowed seccomp syscall to modify own filters |
Low | Critical | seccomp syscall ALLOWED in custom profile |
| Repudiation β Log gaps for non-HTTP traffic | UDP/ICMP traffic not logged by Squid; only kernel dmesg | Low | Medium | [FW_BLOCKED_UDP] goes to kernel buffer, not structured log |
| Info Disclosure β Token read via /proc window | Read /proc/1/environ within 5s window |
Low | Critical | Confirmed 5s sleep before unset |
| Info Disclosure β IPv6 exfiltration | Direct IPv6 TCP/HTTPS to attacker's IPv6 host | Medium | Critical | No ip6tables -A OUTPUT -p tcp -j DROP in container |
| Info Disclosure β Host filesystem read | Agent reads secrets from /host mount |
Medium | High | Full host filesystem mounted at /host |
| DoS β Squid overload | Extremely large number of DNS lookups/CONNECT requests | Low | Low | No rate limiting on proxy connections |
| Elevation β bpf/perf_event_open exploit | Agent uses allowed bpf/perf syscall to read host memory | Low | Critical | bpf, perf_event_open both ALLOWED |
| Elevation β userfaultfd exploitation | Use userfaultfd for race condition kernel exploit | Very Low | Critical | userfaultfd ALLOWED |
π― Attack Surface Map
| Entry Point | Location | Current Protection | Risk |
|---|---|---|---|
| HTTP/HTTPS egress (IPv4) | setup-iptables.sh:236-237 |
DNAT β Squid β domain ACL | β Low |
| HTTP/HTTPS egress (IPv6) | (missing) | No container-level filtering | π΄ Critical |
| DNS queries | setup-iptables.sh:102-122 |
Whitelist to trusted servers only | β Low |
| Non-DNS UDP | setup-iptables.sh |
Host-level only (FW_WRAPPER) | |
| Dangerous ports (22, 25, 3306β¦) | setup-iptables.sh:200-230 |
NAT RETURN + host DROP | β Low |
/proc/1/environ |
entrypoint.sh:611 |
5s window before token unset | |
/host mount |
docker-manager.ts:~200 |
No filesystem restriction | |
| bpf/perf_event_open syscalls | seccomp-profile.json |
Not blocked | π΄ High |
| Shell script construction | entrypoint.sh:AWF_HOST_PATH |
No sanitization | |
| Domain allowlist breadth | domain-patterns.ts |
*.com permitted |
|
| Squid SSL inspection (ssl_bump) | src/ssl-bump.ts |
Opt-in only | β Low |
| API proxy sidecar | containers/api-proxy/ |
Routes through Squid | β Low |
π Evidence Collection
IPv6 TCP bypass β no drop rule in container
$ grep -n "ip6tables.*DROP\|ip6tables.*REJECT\|ip6tables.*tcp" containers/agent/setup-iptables.sh
# Output: only DNS-related ip6tables NAT rules (lines 56, 65-66, 113-114, 134)
# No ip6tables -A OUTPUT -p tcp -j DROP existsIPv6 host chain only created with IPv6 DNS
// src/host-iptables.ts:313
if (ipv6DnsServers.length > 0) {
const ip6tablesAvailable = await isIp6tablesAvailable();
...
await setupIpv6Chain(bridgeName); // β never reached with default configSeccomp defaultAction permissive
$ python3 -c "
import json
with open('containers/agent/seccomp-profile.json') as f: d=json.load(f)
print('defaultAction:', d['defaultAction'])
denied=[n for s in d['syscalls'] for n in s['names']]
print('Total denied:', len(denied))
for s in ['bpf','perf_event_open','userfaultfd','io_uring_setup','seccomp']:
print(s, 'DENIED' if s in denied else 'ALLOWED')"
# defaultAction: SCMP_ACT_ALLOW
# Total denied: 28
# bpf ALLOWED
# perf_event_open ALLOWED
# userfaultfd ALLOWED
# io_uring_setup ALLOWED
# seccomp ALLOWED*.com domain validation bypass
$ node -e "const {validateDomainOrPattern}=require('./dist/domain-patterns.js');
['*.com','*.net','*.io'].forEach(d => {
try { validateDomainOrPattern(d); console.log(d+': ALLOWED'); }
catch(e) { console.log(d+': BLOCKED -',e.message); }
})"
# *.com: ALLOWED
# *.net: ALLOWED
# *.io: ALLOWED
```
</details>
<details>
<summary>npm audit results</summary>
```
$ npm audit
minimatch 10.0.0 - 10.2.2 Severity: high
minimatch has ReDoS (GHSA-7r86-cg39-jmmj, GHSA-23c5-xmqv-rm74)
ajv <6.14.0 || >=7.0.0-alpha.0 <8.18.0 Severity: moderate
ajv has ReDoS when using $data option (GHSA-2g4f-4pwh-qvx6)
2 vulnerabilities (1 moderate, 1 high)β Recommendations
π΄ Critical β Fix Immediately
C1. Add IPv6 TCP drop rule at container level (containers/agent/setup-iptables.sh)
After the existing iptables -A OUTPUT -p tcp -j DROP (line 293), add:
# Block IPv6 TCP (no Squid redirect exists for IPv6)
if [ "$IP6TABLES_AVAILABLE" = true ]; then
ip6tables -A OUTPUT -p tcp -j DROP
ip6tables -A OUTPUT -p udp -j DROP # also block non-DNS UDP
fiC2. Activate host-level IPv6 chain unconditionally
src/host-iptables.ts: The FW_WRAPPER_V6 IPv6 chain should be set up regardless of whether IPv6 DNS servers are configured. Currently, if only IPv4 DNS servers are used (the default), the entire IPv6 filter chain is skipped. The fix: set up FW_WRAPPER_V6 whenever ip6tables is available and insert it into DOCKER-USER, with a default-deny rule for all non-loopback IPv6 traffic.
π High β Fix Soon
H1. Harden the custom seccomp profile
The current profile (containers/agent/seccomp-profile.json) uses defaultAction: SCMP_ACT_ALLOW. This is significantly weaker than Docker's default. Recommended approach: start from the Docker default seccomp profile and add only the specific allowances needed (e.g., mount for procfs in chroot mode). Alternatively, switch defaultAction to SCMP_ACT_ERRNO and build an explicit allowlist.
At minimum, add denials for the highest-risk syscalls that are currently allowed:
{"names":["bpf"],"action":"SCMP_ACT_ERRNO"},
{"names":["perf_event_open"],"action":"SCMP_ACT_ERRNO"},
{"names":["userfaultfd"],"action":"SCMP_ACT_ERRNO"},
{"names":["seccomp"],"action":"SCMP_ACT_ERRNO"}H2. Run npm audit fix to address minimatch and ajv
npm audit fixBoth minimatch and ajv have fixes available. These should be applied.
H3. Consider blocking/warning on TLD-level wildcard patterns
Add a validation check in src/domain-patterns.ts (validateDomainOrPattern) to warn or block patterns like *.com, *.net, *.io β patterns with only 2 segments where the second segment is a known TLD. At minimum, emit a warning to stderr.
π‘ Medium β Plan to Address
M1. Add container-level UDP default-deny
After iptables -A OUTPUT -p tcp -j DROP in setup-iptables.sh, also add:
iptables -A OUTPUT -p udp -j DROPThis ensures container-level UDP blocking is not solely dependent on the host FW_WRAPPER chain.
M2. Reduce the token exposure window
The 5-second sleep before unset_sensitive_tokens is a design tradeoff, but it creates a measurable exposure window. Consider reducing to 1-2 seconds, or use a synchronisation mechanism (e.g., have the one-shot-token library signal readiness via a tmpfs file) rather than a fixed sleep.
M3. Sanitize AWF_HOST_PATH before writing to shell script
In containers/agent/entrypoint.sh, when writing AWF_HOST_PATH to the chroot script, validate that the value contains only safe PATH characters ([a-zA-Z0-9/._:-]) before interpolation. Log an error and use a safe default PATH if validation fails.
π΅ Low β Nice to Have
L1. Use a random suffix for the chroot temp script instead of PID ($$), e.g., $(head -c8 /dev/urandom | xxd -p), to make the filename less predictable.
L2. Scope git config safe.directory '*' β the entrypoint sets this globally for awfuser, which disables Git's safe directory protection entirely. Consider limiting to the specific directories expected to be used.
π Security Metrics
| Metric | Value |
|---|---|
| Source files analyzed | 40+ TypeScript, 5 shell scripts |
| Container scripts reviewed | setup-iptables.sh, entrypoint.sh, docker-stub.sh, api-proxy-health-check.sh |
| Security-critical code paths | 12 (iptables, seccomp, cap-drop, token, domain validation, etc.) |
| Attack surfaces identified | 13 |
| Threats modeled | 9 (STRIDE) |
| Critical findings | 1 |
| High findings | 4 |
| Medium findings | 3 |
| Low findings | 2 |
| npm vulnerabilities | 2 (1 high, 1 moderate) |
| Dependency chain risk | Contained (minimatch/ajv in dev dependencies, not runtime) |
Note: This was intended to be a discussion, but discussions could not be created due to permissions issues. This issue was created as a fallback.
Tip: Discussion creation may fail if the specified category is not announcement-capable. Consider using the "Announcements" category or another announcement-capable category in your workflow configuration.
Generated by Daily Security Review and Threat Modeling
- expires on Mar 10, 2026, 1:51 PM UTC