Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 IDSeverity
cfg-unguarded-sinkHigh/Medium
cfg-auth-gapHigh
cfg-unreachable-sinkMedium
cfg-unreachable-sanitizerLow
cfg-unreachable-sourceLow
cfg-error-fallthroughHigh/Medium
cfg-resource-leakMedium
cfg-lock-not-releasedMedium

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

ScenarioWhyMitigation
Framework middleware authHandler doesn’t call auth directlyExpected; suppress with severity filter or exclude handlers
RAII / defer cleanupImplicit release not visible to CFG (partially handled for Rust Drop and Go defer)Known limitation
Custom guard nameFunction not in the recognised guard listAdd it as a sanitizer rule in config
Test handlersIntentional lack of authDefault non-prod downgrade reduces severity; or exclude test dirs

Common false negatives

ScenarioWhy
Auth in a called helperCross-function guards not tracked
Type-system guardsRust AuthenticatedUser<T> wrappers, typestate patterns not analysed
Cleanup in finally/ensure/defer in callersCross-function cleanup not tracked

Tuning

Recognised guard names

Nyx accepts these patterns as dominating guards:

PatternApplies to
validate*, sanitize*All sinks
check_*, verify_*, assert_*All sinks
shell_escapeShell sinks
html_escapeHTML/XSS sinks
url_encodeURL sinks
whichShell execution (binary lookup)

Recognised auth names

PatternLanguage
is_authenticated, require_auth, check_permission, authorize, authenticate, require_login, check_auth, verify_token, validate_tokenCross-language
middleware.auth, auth.requiredGo
isAuthenticated, checkPermission, hasAuthority, hasRoleJava

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);
}