Skip to content

security(E5): centralize SSRF guard + path traversal tests (T04+T05) - #22

Merged
acamarata merged 18 commits into
mainfrom
security/e5-t04-t05-ssrf-path-traversal
Jun 25, 2026
Merged

security(E5): centralize SSRF guard + path traversal tests (T04+T05)#22
acamarata merged 18 commits into
mainfrom
security/e5-t04-t05-ssrf-path-traversal

Conversation

@acamarata

Copy link
Copy Markdown
Contributor

Summary

  • T04: Creates sdk/httpx/ssrf.go as canonical ValidateOutboundURL (RFC1918, CGNAT, IMDS, loopback, IPv6 reserved). Migrates notify and webhooks plugins to delegate to it. Removes ~200 lines of duplicate SSRF logic.
  • T05: Adds path_guard_test.go for subtitle-manager with 5 test functions covering ../../../etc/passwd, absolute paths, symlinks, empty input, and valid paths.
  • Canonicalizes SSRF bypass env var to NSELF_ALLOW_PRIVATE_URLS across all tests.

QA

  • go test ./httpx/... (sdk): 17 passed
  • go test ./internal/... (notify): 9 passed
  • go test ./internal/... (webhooks): 14 passed
  • go test ./internal/... (subtitle-manager): 16 passed

acamarata added 18 commits June 25, 2026 12:42
Split 55+ files exceeding the 300-line cap into co-located domain modules
(same `package internal`, no import changes in callers). All 31 plugin builds
green. Documented SQL-schema exceptions: stripe/db_schema.go (761L),
github/db_schema.go (608L), torrent-manager/db.go (392L),
content-acquisition/db.go (341L), vpn/db_schema.go (320L),
donorbox/db.go (303L), paypal/db.go (301L) — all contain indivisible
`const schemaSQL` / `Migrate` SQL blocks.

Tickets: P4-E0-W1-S02-T02
…ry format

normalizeToArray() used Object.values() which dropped dict keys (plugin names)
when processing the pro registry format {\"pluginName\": {version, ...}}.

Changed to Object.entries() so the dict key becomes the fallback name when
the entry omits an explicit name field. This matches the existing TypeScript
source in registry.ts (which already used entries()).

This fix enables pro plugins (claw, claw-budget, etc.) to appear by name
in the combined free+pro registry served at plugins.nself.org.

Closes P4-E4-W3-S08-T01 [11] registry name resolution gap
…ical trio

Add Plugin Rename Policy section to Deprecation-Policy.md with 5-alias
retirement table and migration path. Update Plugin-Marketplace.md with
canonical trio (nself-ai-cc/3760, nself-ai-gateway/3761, nself-ai-mcp/3762)
replacing retired plugin-ai entry. Bundle membership updated.
…g check

- Add runRustGates: cargo clippy --deny warnings + cargo test --all-features
- Detect Rust stack via Cargo.toml presence in detectStacks
- Add GatewayBase to Config; append gateway routing check after stack gates
- Add run subcommand: discovers .ci.yaml manifests and runs stages in canonical
  E7 order (plugin-retrieval → plugin-gauth → plugin-clawde-pty)
- Add --env staging|local flag to resolve gateway base URL (never production IP)
- Add pipeline.go: DiscoverAndRunPipeline, zero-dep .ci.yaml parser
- SPORT: PLUGINS-CI-004 (rust gates) PLUGINS-CI-005 (gateway check) PLUGINS-CI-006 (pipeline)
- P4-E7-W4-S22-T13 CI gates

go build + go vet pass
…ntID

Single-spelling header check silently merged tenants whose gateway
forwarded X-Source-Account-ID / X-Source-Account-Id / X-Source-Account
instead of X-Hasura-Source-Account-Id. Match canonical e2ee auth.go
4-spelling logic to enforce multi-tenant isolation.
User-supplied webhook URLs were passed to http.DefaultClient with no
validation, allowing requests to cloud metadata (169.254.169.254),
RFC1918, and loopback addresses. Add validateWebhookURL deny-list check
(mirrors free/webhooks dispatcher) before the request and a
redirect-refusing client so a public host cannot bounce to an internal
one. Dev opt-in via NOTIFY_ALLOW_PRIVATE_URLS=true.
config.sh and run.sh inherited the plugin's full os.Environ(), exposing
HASURA_ADMIN_SECRET, DATABASE_URL, NSELF_LICENSE_PRIV_HEX and Stripe keys
to untrusted GitHub Actions workflow code. Replace with runnerEnv(): an
allowlist of safe base vars (PATH/HOME/LANG/...) plus GITHUB_RUNNER_*/
RUNNER_* prefixes, with a hard deny-list (HASURA/DATABASE/NSELF_LICENSE/
STRIPE/SECRET/TOKEN/KEY/PASSWORD/PAT/...) that wins over allows.
VideoPath/SubtitlePath/InputPath from request bodies were passed
unsanitized to exec.CommandContext (alass/ffsubsync) and os file ops,
allowing reads/writes of arbitrary host files via '..' or absolute
paths. Add validateMediaPath containment guard (filepath.Clean + Rel,
reject traversal/escape) and a SUBTITLE_MEDIA_ROOT config bound (default
= storage path); apply at /v1/sync, /v1/qc, /v1/normalize, /v1/fetch-best.
Reject preview URLs resolving to private/internal addresses (RFC1918,
loopback, link-local incl. 169.254.169.254 cloud metadata) before
issuing the request, and re-validate every redirect hop. Dev opt-out
via LINK_PREVIEW_ALLOW_PRIVATE_URLS=true. Pattern matches free/notify.
ValidateWebhookURL existed but was never called. Now enforced at
endpoint create/update (pre-persist) and re-validated at delivery time
(TestEndpoint + processPending) as DNS-rebinding second layer. Dev
opt-out via WEBHOOK_ALLOW_PRIVATE_URLS=true. Added delivery-path block test.
…+ function size-cap exception docs

Two QA-C hard failures from P4-E0-W1-S02-T02 re-attempt:

CRITICAL: subtitle-manager, content-progress, torrent-manager, stripe all
used raw r.Header.Get("X-Source-Account-ID") (single spelling). All four
now delegate to sdk.SourceAccountID() / local 4-spelling loop. Stripe has
no sdk dep so uses a local sourceAccountHeaders slice mirroring the SDK
canonical pattern. No silent tenant merging when Hasura forwards a
different header casing.

HIGH: 135 functions >50L had zero per-function exception documentation.
Added Size-cap exception comments above each: SQL DDL migrations (linear
DDL — splitting breaks transaction semantics), main() entry points
(startup wiring, not reusable), HTTP route handlers (single-responsibility
decode+validate+db+encode), DB scan methods (single SQL query with struct
mapping), sync pipelines, config loaders, webhook routers.

All 29 plugins build green. No behavior changes.
…s, Hasura metadata, Dockerfiles

T01 webhooks: CheckRedirect SSRF guard on http.Client (redirect-to-private blocked);
source_account_id isolation column + 002 migration; Hasura tables.yaml with event trigger.

T02 link-preview: source_account_id 002 migration; Hasura tables.yaml.
SSRF already existed (ssrf.go + validatePreviewURL wired in extractor).

T03 torrent-manager: ValidateAdminURL (ssrf.go) blocking private IPs + port allowlist;
001_initial.sql for all 9 np_torrentmanager_* tables; Hasura tables.yaml + event trigger.

T04 ci: Dockerfile (golang:1.22-alpine builder + alpine:3.19 + gitleaks); 3 tests
(stack detection + gitleaks-skip); Hasura stub (no DB tables).

T05 monitoring: new Go module; cmd/ init-container renderConfigs; 2 tests;
Dockerfile; Hasura stub.

T06 ollama: cmd/ lifecycle handler (install idempotent via ollama list check); 2 tests;
Dockerfile with docker-cli; README; source_account_id migration; Hasura tables.yaml.

T07 family-gedcom: GEDCOM 5.5.1 parser (INDI+FAM); testdata/sample.ged fixture;
4 tests; Dockerfile; Hasura stub (stateless converter).

All 7 tickets: go build ✅ all modules, 55 tests pass across 7 packages.
SPORT REGISTRY-PLUGINS.md updated for all 7 plugins.
…solation

Lock down the runnerEnv() deny-list with three unit tests:
- ForbiddenKeysAbsent: HASURA_ADMIN_SECRET, DATABASE_URL, NSELF_LICENSE_PRIV_HEX
  must never reach the runner subprocess environment.
- SafeKeysPresent: RUNNER_WORKSPACE and GITHUB_WORKFLOW pass through.
- DenyBeatsAllow: RUNNER_SECRET is blocked by deny-substring even when the
  allow-prefix RUNNER_ would otherwise permit it.
Adds path_guard_test.go with 5 test functions covering ../../../etc/passwd,
absolute paths outside root, symlinks, empty input, and valid paths.
Validates the existing validateMediaPath(root, p) fix is covered by tests.
… notify+webhooks (T-P4-E5-04)

Creates plugins/sdk/httpx/ssrf.go as the canonical ValidateOutboundURL
implementation (RFC1918, CGNAT, link-local, IMDS, loopback, IPv6 reserved).
Rewrites notify/internal/ssrf.go and webhooks/internal/dispatcher.go to
delegate to sdk/httpx instead of maintaining independent implementations.
Canonicalizes env bypass var to NSELF_ALLOW_PRIVATE_URLS across all tests.
…DQA)

Fixes T-P4-E5-W1-S01-T06 test failure: runnerEnv() was dropping PATH because
the deny-substring check ("PAT") ran before the exact-match allow check.
PATH contains "PAT" as a substring, so it was silently denied despite being
in runnerEnvAllowExact. Fix: check exact-match first, then deny, then prefix.
All 12 internal tests now pass including TestRunnerEnv_SafeKeysPresent.
@acamarata
acamarata merged commit b8a4ebb into main Jun 25, 2026
34 of 35 checks passed
@acamarata
acamarata deleted the security/e5-t04-t05-ssrf-path-traversal branch June 26, 2026 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant