Files
Charon/docs/reports/qa_report_pr3.md
GitHub Actions 00a18704e8 fix(uptime): allow RFC 1918 IPs for admin-configured monitors
HTTP/HTTPS uptime monitors targeting LAN addresses (192.168.x.x,
10.x.x.x, 172.16.x.x) permanently reported 'down' on fresh installs
because SSRF protection rejects RFC 1918 ranges at two independent
checkpoints: the URL validator (DNS-resolution layer) and the safe
dialer (TCP-connect layer). Fixing only one layer leaves the monitor
broken in practice.

- Add IsRFC1918() predicate to the network package covering only the
  three RFC 1918 CIDRs; 169.254.x.x (link-local / cloud metadata)
  and loopback are intentionally excluded
- Add WithAllowRFC1918() functional option to both SafeHTTPClient and
  ValidationConfig; option defaults to false so existing behaviour is
  unchanged for every call site except uptime monitors
- In uptime_service.go, pass WithAllowRFC1918() to both
  ValidateExternalURL and NewSafeHTTPClient together; a coordinating
  comment documents that both layers must be relaxed as a unit
- 169.254.169.254 and the full 169.254.0.0/16 link-local range remain
  unconditionally blocked; the cloud-metadata error path is preserved
- 21 new tests across three packages, including an explicit regression
  guard that confirms RFC 1918 blocks are still applied without the
  option set (TestValidateExternalURL_RFC1918BlockedByDefault)

Fixes issues 6 and 7 from the fresh-install bug report.
2026-03-17 21:22:56 +00:00

7.8 KiB
Raw Blame History

QA Security Report — PR-3: RFC 1918 Bypass for Uptime Monitor

Date: 2026-03-17 QA Agent: Security QA Status at Review Start: Implementation complete, Supervisor-approved Final Verdict: APPROVED FOR COMMIT


Summary

PR-3 adds RFC 1918 private-address bypass capability to the uptime monitor system, allowing users to monitor hosts on private network ranges (10.x.x.x, 172.1631.x.x, 192.168.x.x) without disabling SSRF protections globally. The implementation spans three production files and three test files.


Pre-Audit Fix

Issue: The Supervisor identified that TestValidateExternalURL_WithAllowRFC1918_BlocksMetadata in url_validator_test.go should include WithAllowHTTP() to exercise the SSRF IP-level check rather than failing at the scheme check.

Finding: WithAllowHTTP() was already present in the test at audit start. No change required.


Audit Results

1. Build Verification

cd /projects/Charon/backend && go build ./...

Result: PASS — Clean build, zero errors.


2. Targeted Package Tests (PR-3 Files)

internal/network — RFC 1918 tests

Test Result
TestIsRFC1918_RFC1918Addresses (11 subtests) PASS
TestIsRFC1918_NonRFC1918Addresses (9 subtests) PASS
TestIsRFC1918_NilIP PASS
TestIsRFC1918_BoundaryAddresses (5 subtests) PASS
TestIsRFC1918_IPv4MappedAddresses (5 subtests) PASS
TestSafeDialer_AllowRFC1918_ValidationLoopSkipsRFC1918 PASS
TestSafeDialer_AllowRFC1918_BlocksLinkLocal PASS
TestSafeDialer_AllowRFC1918_BlocksLoopbackWithoutAllowLocalhost PASS
TestNewSafeHTTPClient_AllowRFC1918_BlocksSSRFMetadata PASS
TestNewSafeHTTPClient_WithAllowRFC1918_OptionApplied PASS

Package result: ok github.com/Wikid82/charon/backend/internal/network 0.208s

internal/security — RFC 1918 tests

Test Result
TestValidateExternalURL_WithAllowRFC1918_Permits10x PASS
TestValidateExternalURL_WithAllowRFC1918_Permits172_16x PASS
TestValidateExternalURL_WithAllowRFC1918_Permits192_168x PASS
TestValidateExternalURL_WithAllowRFC1918_BlocksMetadata PASS
TestValidateExternalURL_WithAllowRFC1918_BlocksLinkLocal PASS
TestValidateExternalURL_WithAllowRFC1918_BlocksLoopback PASS
TestValidateExternalURL_RFC1918BlockedByDefault PASS
TestValidateExternalURL_WithAllowRFC1918_IPv4MappedIPv6Allowed PASS
TestValidateExternalURL_WithAllowRFC1918_IPv4MappedMetadataBlocked PASS

Package result: ok github.com/Wikid82/charon/backend/internal/security 0.007s

internal/services — RFC 1918 / Private IP tests

Test Result
TestCheckMonitor_HTTP_LocalhostSucceedsWithPrivateIPBypass PASS
TestCheckMonitor_TCP_AcceptsRFC1918Address PASS

Package result: ok github.com/Wikid82/charon/backend/internal/services 4.256s

Targeted total: 21/21 tests pass.


3. Full Backend Coverage Suite

All 30 packages pass. No regressions introduced.

Package Coverage Result
internal/network 92.1% PASS
internal/security 94.1% PASS
internal/services 86.0% PASS
internal/api/handlers 86.3% PASS
internal/api/middleware 97.2% PASS
internal/caddy 96.8% PASS
internal/cerberus 93.8% PASS
internal/crowdsec 86.2% PASS
internal/models 97.5% PASS
internal/server 92.0% PASS
(all other packages) ≥78% PASS

No packages failed. No regressions.

All three PR-3 packages are above the 85% project threshold.


4. Linting

Initial run on the three modified packages revealed one new issue introduced by PR-3 and 17 pre-existing issues in unrelated service files.

New issue in PR-3 code

File Line Issue Action
internal/network/safeclient_test.go:1130 bodyclose — response body not closed Fixed

Fix applied: TestNewSafeHTTPClient_AllowRFC1918_BlocksSSRFMetadata was updated to assign the response and conditionally close the body:

resp, err := client.Get("http://169.254.169.254/latest/meta-data/")
if resp != nil {
    _ = resp.Body.Close()
}

A secondary gosec G104 (unhandled error on Body.Close()) was also resolved by the explicit _ = assignment.

Pre-existing issues (not introduced by PR-3)

17 issues exist in internal/services/ files unrelated to PR-3 (backup_service.go, crowdsec_startup.go, dns_detection_service.go, emergency_token_service.go, mail_service.go, plugin_loader.go, docker_service_test.go, etc.). These are pre-existing and out of scope for this PR.

Final lint state — PR-3 packages

golangci-lint run ./internal/network/... ./internal/security/...
0 issues.

Result: PASS for all PR-3 code.


5. Security Manual Check — Call Site Isolation

grep -rn "WithAllowRFC1918" --include="*.go" .

Expected: WithAllowRFC1918 used only in its definition files and uptime_service.go (2 call sites).

Actual findings:

File Context
internal/network/safeclient.go:259 Definition of WithAllowRFC1918() (network layer option)
internal/security/url_validator.go:161 Definition of WithAllowRFC1918() (security layer option)
internal/services/uptime_service.go:748 Call site 1 — security.WithAllowRFC1918() (URL pre-validation)
internal/services/uptime_service.go:767 Call site 2 — network.WithAllowRFC1918() (dial-time SSRF guard, mirrors line 748)
internal/network/safeclient_test.go Test uses only
internal/security/url_validator_test.go Test uses only

Security assessment:

  • WithAllowRFC1918 is not present in any notification, webhook, DNS, or other service call paths.
  • The two uptime_service.go call sites are correctly paired: the security layer pre-validates the URL hostname, and the network layer enforces the same policy at dial time. This dual-layer approach is the correct defense-in-depth pattern.
  • 169.254.x.x (link-local/cloud metadata), 127.x.x.x (loopback), and IPv4-mapped IPv6 equivalents remain blocked even with AllowRFC1918=true. Confirmed by test coverage.

Result: PASS — Call site isolation confirmed. No scope creep.


6. GORM Security Scan

Skipped per testing.instructions.md gate criteria: PR-3 does not touch backend/internal/models/** or any database/GORM query logic. Trigger condition not met.


7. Pre-Commit Hooks (lefthook)

lefthook run pre-commit
Hook Result
check-yaml PASS
actionlint PASS
end-of-file-fixer PASS
trailing-whitespace PASS
dockerfile-check PASS
shellcheck PASS
File-specific hooks (golangci-lint-fast, go-vet, etc.) Skipped — no staged files (expected behavior)

Result: PASS — All active hooks passed in 7.45s.


Issues Found and Resolved

# Severity File Issue Resolution
1 Low safeclient_test.go:1130 bodyclose: response body not closed in test Fixed — added conditional resp.Body.Close()
2 Low safeclient_test.go:1132 gosec G104: unhandled error on Body.Close() Fixed — added _ = explicit ignore

No security vulnerabilities. No logic defects. No regressions.


Final Verdict

APPROVED FOR COMMIT

All audit steps passed. The two minor lint issues introduced by the new test code have been fixed. The implementation correctly scopes WithAllowRFC1918 to the uptime service only, maintains dual-layer SSRF protection, and does not weaken any other security boundary. All 21 new tests pass. All 30 backend packages pass with zero regressions.