Skip to content

Why polyglot templates

Most scanners describe a detection as data: send this request, match this string, report this finding. cxg supports that too, and YAML templates work exactly that way. It also lets a template be an ordinary program in eleven other languages.

This page explains when that second option earns its cost, and what the cost actually is. It is not an argument that code templates are better. For most checks they are worse.

A YAML template is declarative, so the engine can read it without running it. That buys real things: cxg can list, filter, and search templates by their metadata; the template has no runtime dependency beyond cxg itself; and there is no way for the template to do something its author did not intend, because it cannot do anything except issue requests and match responses.

For a check shaped like “request this path, look for this string, at this status code”, YAML is the right answer and code is overhead.

The limit is not expressiveness in the abstract. It is that a matcher decides whether a response is interesting, and nothing more. A YAML template cannot use what it learned from one response to decide what to do next, and it cannot compute anything.

That rules out three broad classes of detection.

Requests whose target is chosen by an earlier response

Section titled “Requests whose target is chosen by an earlier response”

Matchers evaluate independently against a fixed request list. There is no way to say “parse the ref path out of /.git/HEAD, then fetch that path”.

This distinction matters more than it looks. A pattern match on /.git/HEAD fires on any server that returns 200 with plausible content for every path: a soft-404 handler, a SPA fallback, a catch-all CDN rule. Following the reference and requiring it to resolve is what separates an exposed repository from a server that merely looks like one. That is a false-positive problem in the first case, and one your reader has to triage by hand.

Write your first template builds exactly this detection.

Matchers compare: equality, substring, regex, size, time. They do not compute. So there is no expressing:

  • Cryptographic checks. Verify a JWT’s signature; confirm a token is signed with a known weak key; check whether a certificate’s public key is reused elsewhere; test whether alg: none is accepted, which needs re-signing and resending a modified token.
  • Statistical checks. Measure entropy across a sample of session identifiers to tell a random token from a counter. One token proves nothing; the distribution is the finding.
  • Structural parsing. Read a binary format such as a git index, a serialised object, or a protocol frame, and assert something about its contents.

A regex over a response body can find a token. It cannot tell you the token is predictable.

The difference between “this endpoint appears vulnerable” and “this endpoint is vulnerable, and here is the artifact proving it” is usually a second request that depends on the first. Confirming an SSRF means observing the callback. Confirming an injection means the second, differently-shaped payload behaving as predicted. Those are multi-step by nature.

A code template can carry state between steps and refuse to report anything until the chain completes. That is what the first-template guide demonstrates: three dependent requests, and no finding unless all three hold.

Real costs, not caveats.

Every language you use is a dependency on the scanning host

Section titled “Every language you use is a dependency on the scanning host”

cxg executes a template by invoking its language’s interpreter or toolchain. If that program is not on PATH, the template cannot run.

A Python template needs python3, and any library it imports. A Go template needs the Go compiler unless a cached build already exists. Writing detections in eleven languages means a scanning host that can run eleven languages, and a container image that carries all of them.

This is also why the templates you write for yourself should use as few third-party libraries as possible. A template that imports requests will not run on a host that has Python but not requests.

A missing runtime looks exactly like a clean scan

Section titled “A missing runtime looks exactly like a clean scan”

This is the sharp edge, and it deserves more attention than it usually gets.

When a template’s runtime is missing, cxg logs a warning, records no finding, and exits 0. The default output is indistinguishable from a scan that ran everything and found nothing:

Findings by Severity:
CRITICAL: 0
HIGH: 0
MEDIUM: 0
LOW: 0
INFO: 0
TOTAL: 0

The reason only appears with -v:

WARN cert_x_gen::executor: Template php-probe failed for target 127.0.0.1:
Execution error: Failed to execute command: No such file or directory (os error 2)
WARN cert_x_gen::executor: Template go-probe3 failed for target 127.0.0.1:
Execution error: Go compiler not found

Both were produced by running cxg against a host missing PHP, and against a Go template the toolchain had never compiled, with go removed from PATH. Each warning is one line; the timestamp and thread id that precede it on every log line are cut here, and the wrap is ours.

A false negative that reports as a clean result is worse than a crash. In CI, run scans with -v and treat template execution failures as build failures. A declarative YAML template cannot fail this way, because it has no runtime to be missing.

Templates in C, C++, Go, Java, and Rust are compiled before they run. cxg caches the build, and only recompiles when the cached binary is missing or older than the source. With go removed from PATH, a Go template compiled by an earlier run still executed from that cache, while a copy of it saved under a new name, never compiled and so never cached, failed with Go compiler not found in the same scan.

The practical consequence is that a cold scanning host, a fresh CI container, pays compile time that a warm one does not, and needs the toolchains present even for templates that will later run from cache.

Templates run with your privileges, unsandboxed

Section titled “Templates run with your privileges, unsandboxed”

This is the most important cost, and the one to understand before running a template you did not write. There is no boundary between cxg and the template it runs: the engine launches the interpreter or compiled binary as a child process and reads its stdout. A probe run on cxg 1.3.0 read the invoking user’s ~/.ssh, spawned a child process, and resolved an external host. Everything the user could do, the template could do.

Neither any config key nor the cxg sandbox command changes this. The full mechanism, the probe evidence, and the mitigations that actually apply are the template trust model. Read it before running third-party templates. In brief: place your isolation boundary outside cxg (a container or VM, an unprivileged user), review code templates as the scripts they are, and prefer YAML for anything you have not read.

Reach for a code template when the check needs to remember something between requests, compute something, or prove something. Use YAML when it does not.

Then account for the cost: the runtime must exist on every host that runs the scan, its absence is silent, and the template runs with your privileges.