You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This report covers a comprehensive, evidence-based security review of the gh-aw-firewall repository, conducted against the codebase on 2026-03-04. All findings are backed by specific file paths, line numbers, and command outputs.
Security posture: Moderate — well-layered defense-in-depth with several gaps that should be addressed.
No previous run of a dedicated "firewall-escape-test" workflow was found in the repository's workflow definitions. The security-review workflow itself is the primary security analysis mechanism. Log download was unavailable (unauthenticated environment), so this review is based entirely on fresh codebase analysis.
🛡️ Architecture Security Analysis
Network Security Assessment
Strengths:
Defense-in-depth: two independent enforcement layers
Container-level (containers/agent/setup-iptables.sh): NAT DNAT redirects HTTP/HTTPS to Squid; TCP DROP default
Host-level (src/host-iptables.ts): DOCKER-USER chain via FW_WRAPPER/FW_WRAPPER_V6 rejects non-allowed traffic
DNS restricted to whitelisted servers only (default: Google DNS 8.8.8.8/8.8.4.4), preventing DNS-over-UDP exfiltration
Dangerous ports blocked in both Squid ACLs and iptables NAT pre-routing (defense-in-depth)
Multicast and link-local traffic rejected at host level (224.0.0.0/4, ff00::/8)
Fixed network topology (172.30.0.0/24) with predictable IPs makes Squid IP bypass hard
ICMP fully blocked at host level via the FW_WRAPPER default-deny rule 8
Gaps:
UDP not explicitly blocked at container level (see Finding F3)
IPv6 fallback when ip6tables is unavailable (see Finding F5)
Container Security Assessment
Strengths:
NET_ADMIN + SYS_CHROOT + SYS_ADMIN dropped via capsh --drop=cap_net_admin,cap_sys_chroot,cap_sys_admin in entrypoint.sh before any user code runs
The custom seccomp profile uses "defaultAction": "SCMP_ACT_ALLOW" — a blocklist approach where all syscalls are allowed unless explicitly denied. Only 28 syscalls are blocked. Docker's built-in default profile uses the opposite approach: SCMP_ACT_ERRNO (deny-by-default) with an allowlist of ~300+ safe syscalls.
Dangerous syscalls NOT blocked by this profile:
Syscall
Risk
mount
Mount arbitrary filesystems (allowed intentionally for procfs in entrypoint, but remains accessible in user code via seccomp — only CAP_SYS_ADMIN blocks it)
unshare
Create new namespaces; --user namespace doesn't require capabilities on many kernels
clone
With CLONE_NEWNET flag, could create a new network namespace (requires CAP_NET_ADMIN, which IS dropped)
bpf
Load eBPF programs for network interception (requires CAP_BPF or CAP_SYS_ADMIN; blocked by capability drop, but defense-in-depth is weakened)
io_uring has been a source of container escapes (CVE-2023-2163, others)
setns
Join existing namespaces
kcmp
Compare kernel objects between processes
Why it matters: Although no-new-privileges:true and capability dropping provide compensating controls, the seccomp profile is the primary syscall-level boundary. Replacing Docker's hardened default profile with a permissive one significantly reduces the syscall-level attack surface protection.
Recommendation: Replace the custom profile with either Docker's default profile (by removing the seccomp= security option) or change defaultAction to SCMP_ACT_ERRNO and provide an explicit allowlist of required syscalls.
F2 — HIGH: SYS_ADMIN Capability During Container Initialization
File:src/docker-manager.ts:870
cap_add: ['NET_ADMIN','SYS_CHROOT','SYS_ADMIN'],
The agent container starts with SYS_ADMIN, one of the most powerful Linux capabilities (equivalent to "superuser for the kernel"). It is needed for mounting procfs at /host/proc in entrypoint.sh. However, there is a race condition window between container start and the capsh --drop call in the entrypoint where the full SYS_ADMIN capability is available.
Attack vector: If an attacker can inject code into the container before the capsh drop (e.g., via a malicious Docker image, a compromised base layer, or a vulnerability in the entrypoint initialization steps that run before the drop), they would have SYS_ADMIN access.
Mitigating factors:
Container images are pulled from GHCR (trusted registry)
Entrypoint runs as root, then drops to awfuser — standard pattern
no-new-privileges:true prevents SUID escalation after drop
Recommendation: Consider using a minimal setup container that only mounts the procfs, then use nsenter or Docker's --init to avoid needing SYS_ADMIN in the main agent container. At minimum, document this as a known risk in the security documentation.
F3 — HIGH: npm Dependency Vulnerabilities
Evidence:
$ npm audit
# minimatch 10.0.0 - 10.2.2# Severity: HIGH# minimatch has ReDoS: matchOne() combinatorial backtracking# GHSA-7r86-cg39-jmmj, GHSA-23c5-xmqv-rm74# fix available via npm audit fix## 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
minimatch (used by development tooling) has two ReDoS vulnerabilities. While these are in dev dependencies (commitlint), they run in CI pipelines and could affect automated workflows.
Recommendation: Run npm audit fix to resolve both. This is a low-effort fix.
F4 — MEDIUM: UDP Not Explicitly Blocked at Container Level
File:containers/agent/setup-iptables.sh:293
# Container OUTPUT filter chain — only blocks TCP:
iptables -A OUTPUT -p tcp -j DROP
# No corresponding: iptables -A OUTPUT -p udp -j DROP
The container's iptables OUTPUT filter only drops TCP. Non-DNS UDP traffic (ports other than 53) is not blocked at the container iptables level. UDP-based protocols (e.g., QUIC/HTTP3 on port 443 UDP, NTP, SNMP, custom UDP exfiltration channels) would bypass the container-level firewall.
Mitigating factor: The host-level FW_WRAPPER chain (src/host-iptables.ts:479-491) does block all other UDP. However, this creates a single point of enforcement for UDP — if the host chain were to fail, be bypassed, or not set up before container traffic flows, UDP would be unrestricted.
Recommendation: Add a UDP DROP rule in setup-iptables.sh after the DNS ACCEPT rules:
# After DNS allow rules and before end of script
iptables -A OUTPUT -p udp -j DROP
F5 — MEDIUM: IPv6 Egress Uncontrolled When ip6tables Unavailable
if [ "$IP6TABLES_AVAILABLE"=true ];then# ... IPv6 ruleselseecho"[iptables] WARNING: ip6tables is not available, IPv6 rules will be skipped"fi
When ip6tables is unavailable inside the container, all IPv6 outbound traffic bypasses the Squid proxy and is not subject to domain whitelisting. An attacker who knows the IPv6 address of a target can make unrestricted outbound connections.
Context: The host-level FW_WRAPPER_V6 chain provides some protection if ip6tables is available at the host level. However, if both container and host lack ip6tables, IPv6 is completely unrestricted.
Recommendation: If ip6tables is unavailable, the container should either:
Block all IPv6 output using ip6tables (fail-safe)
Or disable IPv6 in the container network configuration via sysctl -w net.ipv6.conf.all.disable_ipv6=1
F6 — MEDIUM: Wildcard Domain Validation Edge Case
File:src/domain-patterns.ts:164-170
constwildcardSegments=segments.filter(s=>s==='*').length;consttotalSegments=segments.length;if(wildcardSegments>1&&wildcardSegments>=totalSegments-1){thrownewError(`Pattern '\$\{trimmed}' has too many wildcard segments ...`);}
A pattern like *.b.*.d.com has wildcardSegments=2, totalSegments=5. The check evaluates: 2 > 1 (true) AND 2 >= 4 (false) → pattern is accepted. This pattern would match anything.b.anything.d.com — a very broad domain set.
Recommendation: Tighten the validation to reject patterns with more than one wildcard segment:
if(wildcardSegments>1){thrownewError(`Pattern '\$\{trimmed}' has multiple wildcard segments and is not allowed`);}
F7 — MEDIUM: URL Pattern Values Embedded in Squid Config Without Sanitization
URL patterns from --allow-urls are embedded directly into the Squid configuration as url_regex ACL values. While basic validation occurs (must start with https://, must have a path, broad pattern rejection), Squid-specific injection characters (newlines, null bytes, config directives) are not sanitized. A crafted pattern like https://github.com/org/*\nhttp_access allow all could inject Squid directives if the newline check is incomplete.
Evidence: The validation in src/cli.ts:1004-1034 checks for broad patterns (.*) and path presence, but does not sanitize \n, \r, or other config-breaking characters.
Recommendation: Sanitize URL patterns to strip non-printable characters and newlines before embedding them in Squid config:
constsanitized=pattern.replace(/[\r\n\0]/g,'');
F8 — LOW: Container-Level Logs May Not Capture All UDP Blocked Events
File:containers/agent/setup-iptables.sh — no UDP LOG rule
The container's iptables does not include LOG rules for blocked UDP (since UDP blocking is handled at the host level). This means UDP blocking events only appear in host-level kernel logs (dmesg), not in container-accessible logs. For forensics, UDP exfiltration attempts would require host log access.
F9 — LOW: Predictable Work Directory Pattern
File:src/cli.ts:707
path.join(os.tmpdir(),`awf-\$\{Date.now()}`)
The work directory follows a predictable timestamp-based pattern (/tmp/awf-(milliseconds)). A malicious process running on the same host could predict or race to create/modify the work directory before AWF writes its configuration files, potentially injecting malicious squid.conf or docker-compose.yml content.
Mitigating factors: Requires local host access (same privilege level), and container file integrity checks during startup would catch obvious tampering.
Recommendation: Use crypto.randomUUID() or a cryptographically random suffix instead of Date.now().
⚠️ Threat Model (STRIDE)
Threat
Category
Vector
Likelihood
Impact
Severity
Mitigated By
DNS exfiltration via unauthorized server
Info Disclosure
UDP/TCP to non-whitelisted DNS
Low
Medium
Medium
Trusted DNS allowlist + UDP block
IPv6 bypass when ip6tables unavailable
Info Disclosure
Direct IPv6 outbound
Medium
High
High
Host-level ip6tables (when available)
ICMP tunneling
Info Disclosure
ICMP echo to external
Low
Medium
Medium
Host FW_WRAPPER default-deny rule 8
UDP-based exfiltration
Info Disclosure
UDP to non-DNS server
Medium
High
High
Host-level FW_WRAPPER UDP block
Wildcard domain bypass
Spoofing
Broad *.x.*.y.com pattern
Low
Medium
Medium
F6 validation edge case
Squid config injection via URL pattern
Tampering
\n in --allow-urls value
Low
High
Medium
Basic validation exists
Seccomp bypass via allowed syscalls
Elevation
io_uring, bpf, userfaultfd
Low-Medium
High
High
Capability drops (post-setup)
SYS_ADMIN exploit during initialization
Elevation
Malicious entrypoint hook
Very Low
Critical
High
GHCR image provenance
Work dir race condition
Tampering
/tmp/awf-(timestamp)
Very Low
High
Low
Requires local host access
ReDoS via minimatch
DoS
Malformed glob pattern in CI
Very Low
Low
Low
Dev dependency only
🎯 Attack Surface Map
#
Entry Point
Location
What It Does
Current Protections
Risk
1
--allow-domains CLI flag
src/cli.ts:870
Domain whitelist input
validateDomainOrPattern(), regex escaping
Low
2
--allow-urls CLI flag
src/cli.ts:989-1040
URL pattern for Squid regex ACL
Basic format validation, broad-pattern check
Medium (F7)
3
--allow-host-ports
src/cli.ts:778
Opens ports to host gateway
Dangerous port list, --enable-host-access required
Low
4
Container iptables OUTPUT chain
setup-iptables.sh:293
TCP drop, NAT to Squid
TCP DROP; UDP not dropped at container level
Medium (F4)
5
Host DOCKER-USER chain
src/host-iptables.ts
Egress filtering for all containers
Default-deny with specific allows
Low
6
Squid proxy ACL rules
src/squid-config.ts
Domain + port filtering
dstdomain + dstdom_regex, DANGEROUS_PORTS list
Low
7
Seccomp profile
containers/agent/seccomp-profile.json
Syscall filtering
28-syscall blocklist (weak)
Critical (F1)
8
Container capabilities
src/docker-manager.ts:870,884
Privilege controls
capsh drop + no-new-privileges
High (F2)
9
IPv6 egress
setup-iptables.sh
IPv6 traffic control
Conditional (ip6tables availability)
Medium (F5)
✅ Recommendations (Prioritized)
🔴 Critical — Address Immediately
1. Replace permissive seccomp profile with a deny-by-default allowlist
Option A (simplest): Remove the custom seccomp profile and rely on Docker's hardened default:
Remove seccomp=\$\{config.workDir}/seccomp-profile.json from security_opt in src/docker-manager.ts:885
Option B (preserve custom blocks): Change defaultAction to SCMP_ACT_ERRNO and add an allowlist:
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
📊 Executive Summary
This report covers a comprehensive, evidence-based security review of the
gh-aw-firewallrepository, conducted against the codebase on 2026-03-04. All findings are backed by specific file paths, line numbers, and command outputs.Security posture: Moderate — well-layered defense-in-depth with several gaps that should be addressed.
🔍 Context: Previous Firewall Escape Test Agent
No previous run of a dedicated "firewall-escape-test" workflow was found in the repository's workflow definitions. The
security-reviewworkflow itself is the primary security analysis mechanism. Log download was unavailable (unauthenticated environment), so this review is based entirely on fresh codebase analysis.🛡️ Architecture Security Analysis
Network Security Assessment
Strengths:
containers/agent/setup-iptables.sh): NAT DNAT redirects HTTP/HTTPS to Squid; TCP DROP defaultsrc/host-iptables.ts): DOCKER-USER chain viaFW_WRAPPER/FW_WRAPPER_V6rejects non-allowed traffic224.0.0.0/4,ff00::/8)172.30.0.0/24) with predictable IPs makes Squid IP bypass hardGaps:
Container Security Assessment
Strengths:
NET_ADMIN+SYS_CHROOT+SYS_ADMINdropped viacapsh --drop=cap_net_admin,cap_sys_chroot,cap_sys_admininentrypoint.shbefore any user code runsno-new-privileges:truesecurity option prevents SUID/SGID privilege escalation (src/docker-manager.ts:884)NET_RAWdropped from both Squid and agent containers, preventing raw socket creation and ping-based exfiltrationGaps:
Domain Validation Assessment
Strengths:
*,*.*) explicitly rejected (src/domain-patterns.ts:127-135)DOMAIN_CHAR_PATTERN = '[a-zA-Z0-9.-]*'used instead of.*in wildcard-to-regex conversion (src/domain-patterns.ts:68-72)src/domain-patterns.ts:248)Gaps:
Input Validation Assessment
Strengths:
'\$\{arg.replace(/'/g, "'\\''")}'(src/cli.ts:509)src/docker-manager.ts:31)https://prefix, path component, and broad-pattern checkGaps:
F1 — CRITICAL: Permissive Seccomp Profile (Allow-by-Default)
File:
containers/agent/seccomp-profile.jsonEvidence:
The custom seccomp profile uses
"defaultAction": "SCMP_ACT_ALLOW"— a blocklist approach where all syscalls are allowed unless explicitly denied. Only 28 syscalls are blocked. Docker's built-in default profile uses the opposite approach:SCMP_ACT_ERRNO(deny-by-default) with an allowlist of ~300+ safe syscalls.Dangerous syscalls NOT blocked by this profile:
mountunshare--usernamespace doesn't require capabilities on many kernelscloneCLONE_NEWNETflag, could create a new network namespace (requires CAP_NET_ADMIN, which IS dropped)bpfperf_event_openuserfaultfdio_uring_setupsetnskcmpWhy it matters: Although
no-new-privileges:trueand capability dropping provide compensating controls, the seccomp profile is the primary syscall-level boundary. Replacing Docker's hardened default profile with a permissive one significantly reduces the syscall-level attack surface protection.Recommendation: Replace the custom profile with either Docker's default profile (by removing the
seccomp=security option) or changedefaultActiontoSCMP_ACT_ERRNOand provide an explicit allowlist of required syscalls.F2 — HIGH: SYS_ADMIN Capability During Container Initialization
File:
src/docker-manager.ts:870The agent container starts with
SYS_ADMIN, one of the most powerful Linux capabilities (equivalent to "superuser for the kernel"). It is needed for mounting procfs at/host/procinentrypoint.sh. However, there is a race condition window between container start and thecapsh --dropcall in the entrypoint where the fullSYS_ADMINcapability is available.Attack vector: If an attacker can inject code into the container before the
capshdrop (e.g., via a malicious Docker image, a compromised base layer, or a vulnerability in the entrypoint initialization steps that run before the drop), they would haveSYS_ADMINaccess.Mitigating factors:
awfuser— standard patternno-new-privileges:trueprevents SUID escalation after dropRecommendation: Consider using a minimal setup container that only mounts the procfs, then use
nsenteror Docker's--initto avoid needingSYS_ADMINin the main agent container. At minimum, document this as a known risk in the security documentation.F3 — HIGH: npm Dependency Vulnerabilities
Evidence:
minimatch(used by development tooling) has two ReDoS vulnerabilities. While these are in dev dependencies (commitlint), they run in CI pipelines and could affect automated workflows.Recommendation: Run
npm audit fixto resolve both. This is a low-effort fix.F4 — MEDIUM: UDP Not Explicitly Blocked at Container Level
File:
containers/agent/setup-iptables.sh:293The container's iptables OUTPUT filter only drops TCP. Non-DNS UDP traffic (ports other than 53) is not blocked at the container iptables level. UDP-based protocols (e.g., QUIC/HTTP3 on port 443 UDP, NTP, SNMP, custom UDP exfiltration channels) would bypass the container-level firewall.
Mitigating factor: The host-level
FW_WRAPPERchain (src/host-iptables.ts:479-491) does block all other UDP. However, this creates a single point of enforcement for UDP — if the host chain were to fail, be bypassed, or not set up before container traffic flows, UDP would be unrestricted.Recommendation: Add a UDP DROP rule in
setup-iptables.shafter the DNS ACCEPT rules:# After DNS allow rules and before end of script iptables -A OUTPUT -p udp -j DROPF5 — MEDIUM: IPv6 Egress Uncontrolled When ip6tables Unavailable
File:
containers/agent/setup-iptables.sh(multiple IPv6 guards)When
ip6tablesis unavailable inside the container, all IPv6 outbound traffic bypasses the Squid proxy and is not subject to domain whitelisting. An attacker who knows the IPv6 address of a target can make unrestricted outbound connections.Context: The host-level
FW_WRAPPER_V6chain provides some protection if ip6tables is available at the host level. However, if both container and host lack ip6tables, IPv6 is completely unrestricted.Recommendation: If ip6tables is unavailable, the container should either:
sysctl -w net.ipv6.conf.all.disable_ipv6=1F6 — MEDIUM: Wildcard Domain Validation Edge Case
File:
src/domain-patterns.ts:164-170A pattern like
*.b.*.d.comhaswildcardSegments=2,totalSegments=5. The check evaluates:2 > 1(true) AND2 >= 4(false) → pattern is accepted. This pattern would matchanything.b.anything.d.com— a very broad domain set.Recommendation: Tighten the validation to reject patterns with more than one wildcard segment:
F7 — MEDIUM: URL Pattern Values Embedded in Squid Config Without Sanitization
File:
src/squid-config.ts:117-120URL patterns from
--allow-urlsare embedded directly into the Squid configuration asurl_regexACL values. While basic validation occurs (must start withhttps://, must have a path, broad pattern rejection), Squid-specific injection characters (newlines, null bytes, config directives) are not sanitized. A crafted pattern likehttps://github.com/org/*\nhttp_access allow allcould inject Squid directives if the newline check is incomplete.Evidence: The validation in
src/cli.ts:1004-1034checks for broad patterns (.*) and path presence, but does not sanitize\n,\r, or other config-breaking characters.Recommendation: Sanitize URL patterns to strip non-printable characters and newlines before embedding them in Squid config:
F8 — LOW: Container-Level Logs May Not Capture All UDP Blocked Events
File:
containers/agent/setup-iptables.sh— no UDP LOG ruleThe container's iptables does not include
LOGrules for blocked UDP (since UDP blocking is handled at the host level). This means UDP blocking events only appear in host-level kernel logs (dmesg), not in container-accessible logs. For forensics, UDP exfiltration attempts would require host log access.F9 — LOW: Predictable Work Directory Pattern
File:
src/cli.ts:707The work directory follows a predictable timestamp-based pattern (
/tmp/awf-(milliseconds)). A malicious process running on the same host could predict or race to create/modify the work directory before AWF writes its configuration files, potentially injecting malicious squid.conf or docker-compose.yml content.Mitigating factors: Requires local host access (same privilege level), and container file integrity checks during startup would catch obvious tampering.
Recommendation: Use
crypto.randomUUID()or a cryptographically random suffix instead ofDate.now().*.x.*.y.compattern\nin--allow-urlsvalueio_uring,bpf,userfaultfd/tmp/awf-(timestamp)🎯 Attack Surface Map
--allow-domainsCLI flagsrc/cli.ts:870validateDomainOrPattern(), regex escaping--allow-urlsCLI flagsrc/cli.ts:989-1040--allow-host-portssrc/cli.ts:778--enable-host-accessrequiredsetup-iptables.sh:293src/host-iptables.tssrc/squid-config.tscontainers/agent/seccomp-profile.jsonsrc/docker-manager.ts:870,884setup-iptables.sh✅ Recommendations (Prioritized)
🔴 Critical — Address Immediately
1. Replace permissive seccomp profile with a deny-by-default allowlist
Option A (simplest): Remove the custom seccomp profile and rely on Docker's hardened default:
seccomp=\$\{config.workDir}/seccomp-profile.jsonfromsecurity_optinsrc/docker-manager.ts:885Option B (preserve custom blocks): Change
defaultActiontoSCMP_ACT_ERRNOand add an allowlist:{ "defaultAction": "SCMP_ACT_ERRNO", "syscalls": [ { "names": ["read", "write", "open", "close", "...300+ safe syscalls..."], "action": "SCMP_ACT_ALLOW" } ] }Docker's default profile can be used as a starting point and the specific blocks for
ptrace,keyctl, etc. preserved as additional hardening.🟠 High — Address Soon
2. Add UDP DROP at container iptables level (
containers/agent/setup-iptables.sh)After the DNS ACCEPT rules (line ~280), add before the TCP DROP:
# Block all non-DNS UDP traffic iptables -A OUTPUT -p udp -j DROP3. Fix IPv6 handling when ip6tables unavailable (
containers/agent/setup-iptables.sh)Add a hard-fail or sysctl disable when ip6tables is unavailable:
4. Update vulnerable npm dependencies (root directory)
This resolves both
minimatch(high, ReDoS) andajv(moderate, ReDoS).🟡 Medium — Plan to Address
5. Tighten multi-wildcard domain validation (
src/domain-patterns.ts:164-170)6. Sanitize URL patterns for Squid config injection (
src/squid-config.ts:117-120)🟢 Low — Nice to Have
7. Use cryptographically random work directory suffix (
src/cli.ts:707)8. Add container-level UDP logging (
containers/agent/setup-iptables.sh)Before the UDP DROP rule, add a LOG rule for forensic visibility:
iptables -A OUTPUT -p udp -j LOG --log-prefix "[FW_BLOCKED_UDP_CONTAINER] " --log-level 49. Document SYS_ADMIN initialization risk in security documentation
Add an explicit note in
docs/explaining the SYS_ADMIN capability window during initialization and the compensating controls that make it safe.📋 Evidence Collection
Seccomp profile analysis
npm audit output
Container iptables OUTPUT chain (UDP gap)
Host FW_WRAPPER chain structure
Wildcard validation logic
📈 Security Metrics
npm audit fix, F4: add UDP DROP, F7: sanitize URL patterns)no-new-privileges:true, capability dropping viacapsh)Beta Was this translation helpful? Give feedback.
All reactions