#CFG structural analysis

Nyx builds an intra-procedural control-flow graph per function and checks structural properties: whether sinks are guarded by sanitizers or validators, whether web handlers check authentication, whether resources are released on all exit paths, and whether error paths terminate before reaching dangerous code.

These detectors use dominator analysis. A guard dominates a sink when the guard must execute before the sink on every path from entry.

#Rule IDs

Rule ID Severity
cfg-unguarded-sink High/Medium
cfg-auth-gap High
cfg-unreachable-sink Medium
cfg-unreachable-sanitizer Low
cfg-unreachable-source Low
cfg-error-fallthrough High/Medium
cfg-resource-leak Medium
cfg-lock-not-released Medium

#What it detects

cfg-unguarded-sink: A sink call (system, eval, Command::new, db.execute, etc.) is reachable from function entry without passing through any guard or sanitizer that matches the sink's capability.

cfg-auth-gap: A function identified as a web handler (by parameter naming conventions like req, res, ctx, request, language-dependent) reaches a privileged sink (shell execution, file I/O) without a preceding authentication call.

cfg-unreachable-*: Sinks, sanitizers, or sources in dead code. Usually signals a refactoring error that silently disabled security-relevant logic.

cfg-error-fallthrough: An error-handling branch (null check, error-return check) does not terminate. Execution falls through to a dangerous operation on the error path.

cfg-resource-leak, cfg-lock-not-released: A resource acquisition (File::open, fopen, socket, Lock) is not matched by a release on every exit path from the function.

#What it can't detect

  • Inter-procedural guards. Middleware-level auth, helper functions that internally call auth, and cleanup performed in a caller are invisible.
  • Dynamic dispatch. Virtual calls, function pointers, closures resolve to no specific callee.
  • Correctness of guards. The detector checks a guard dominates the sink. It cannot check the guard is correct. A no-op if true {} would suppress the finding.
  • Custom validation logic. Only recognised guard names are checked. if password == expected is not a recognised guard.
  • Cross-function resource flows. If a file handle opens in one function and closes in another, the opener gets flagged as a leak. This is the largest source of FPs on factory-pattern code.

#Common false positives

Scenario Why Mitigation
Framework middleware auth Handler doesn't call auth directly Expected; suppress with severity filter or exclude handlers
RAII / defer cleanup Implicit release not visible to CFG (partially handled for Rust Drop and Go defer) Known limitation
Custom guard name Function not in the recognised guard list Add it as a sanitizer rule in config
Test handlers Intentional lack of auth Default non-prod downgrade reduces severity; or exclude test dirs

#Common false negatives

Scenario Why
Auth in a called helper Cross-function guards not tracked
Type-system guards Rust AuthenticatedUser<T> wrappers, typestate patterns not analysed
Cleanup in finally/ensure/defer in callers Cross-function cleanup not tracked

#Tuning

#Recognised guard names

Nyx accepts these patterns as dominating guards:

Pattern Applies to
validate*, sanitize* All sinks
check_*, verify_*, assert_* All sinks
shell_escape Shell sinks
html_escape HTML/XSS sinks
url_encode URL sinks
which Shell execution (binary lookup)

#Recognised auth names

Pattern Language
is_authenticated, require_auth, check_permission, authorize, authenticate, require_login, check_auth, verify_token, validate_token Cross-language
middleware.auth, auth.required Go
isAuthenticated, checkPermission, hasAuthority, hasRole Java

For Rust auth checks (require_*, ownership equality, row-level checks), see auth.md.

#Custom guards

[[analysis.languages.python.rules]]
matchers = ["validate_request", "check_csrf"]
kind = "sanitizer"
cap  = "all"

#Custom auth functions

[[analysis.languages.javascript.rules]]
matchers = ["ensureLoggedIn", "requirePermission"]
kind = "sanitizer"
cap  = "all"

#Examples

Unguarded sink:

func handler(w http.ResponseWriter, r *http.Request) {
    cmd := r.URL.Query().Get("cmd")
    exec.Command("sh", "-c", cmd).Run()  // cfg-unguarded-sink
}

Auth gap:

app.get('/admin/delete', (req, res) => {
    // No auth call
    db.execute("DELETE FROM users WHERE id = " + req.params.id);  // cfg-auth-gap
});

Resource leak:

void process() {
    FILE *f = fopen("data.txt", "r");
    if (error) {
        return;           // cfg-resource-leak: f not closed on this path
    }
    fclose(f);
}