Skip to content

[Security Review] Daily Security Review and Threat Modeling β€” 2026-03-03Β #1139

@github-actions

Description

@github-actions

πŸ“Š 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:

  1. Host-level (src/host-iptables.ts): DOCKER-USER chain filtering via FW_WRAPPER custom chain
  2. 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, then REJECT)
  • 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 rules

setup-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 here

Default 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_ADMIN added for setup, then explicitly dropped via capsh --drop=cap_net_admin before user code runs (entrypoint.sh lines 639, 648)
  • NET_RAW dropped at Docker Compose level preventing raw socket bypass
  • no-new-privileges security 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 rules

The 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_PID

Tokens 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: ALLOWED

The 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 in src/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) ⚠️ Medium
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 ⚠️ Medium
/host mount docker-manager.ts:~200 No filesystem restriction ⚠️ Medium
bpf/perf_event_open syscalls seccomp-profile.json Not blocked πŸ”΄ High
Shell script construction entrypoint.sh:AWF_HOST_PATH No sanitization ⚠️ Medium
Domain allowlist breadth domain-patterns.ts *.com permitted ⚠️ Medium
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 exists
IPv6 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 config
Seccomp 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
fi

C2. 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 fix

Both 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 DROP

This 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions