OpenAPI Extensions for Security Testing: x-source and x-nv-dast-sinks Explained

OpenAPI extensions are fields beginning with x- that any tool can add to an operation. For security testing, two do the most work: one records the file and line that declared the route, and one lists what the handler's code can reach, such as SQL. In one 41-operation Spring app, 13 reached SQL.

Key takeaways

  • A standard OpenAPI document describes the contract: paths, parameters, and schemas. It says nothing about what the handler does with the input.
  • x-source records where a route was declared, as file~~line. It is the entry point, not the flaw.
  • x-nv-dast-sinks lists the classes of sensitive operation reachable from the handler: SQL, OS commands, outbound requests, redirects, templates, uploads, and more. Its tags describe operations, not vulnerabilities.
  • sql-raw and sql-builder separate statements the API does not parameterize from ones it does. That one distinction turns a spec into a review queue.
  • The tags mark reachability, the weakest of the three levels of evidence. A missing tag means "not found," never "safe."

What a spec says, and what it leaves out

An OpenAPI document is a contract. It lists every path, the methods each accepts, the parameters and their types, and the shapes of the request and response bodies. That is enough for a client to call the API and for a scanner to know what to send.

It is not enough to know where to look. Two endpoints with identical signatures, each taking one string and returning a list, can do completely different things with that string. One hands it to a parameterized query. The other concatenates it into SQL. The contract cannot tell them apart, because the difference lives in the handler, and the contract was never meant to describe the handler. That gap is what OpenAPI extensions for security testing fill.

The OpenAPI Specification leaves room for exactly this. Any field whose name begins with x- is a specification extension, and tools are free to define their own. A spec generated from source code can use that room to carry what only the source knows.

x-source: where the route was declared

When NightVision's source-based API discovery generates a spec with nightvision swagger extract, every operation it finds carries an x-source field: the file and line of the route declaration, joined by ~~.

/api/jwt/users/search/bad/{text}:
  get:
    x-source: src/main/java/hawk/api/jwt/JwtUserController.java~~42

The value is an address. It tells a reviewer, a developer, or a coding agent which handler a request enters, without a search through the repository. It is the field behind source-linked DAST: on API scans of a source-discovered spec, a runtime finding can carry the file and line of the endpoint it came through.

Keep its limits in view. The line is the entry point, where the route is declared. As Sources and Sinks, Explained puts it, the entry point is an address, not a diagnosis. The flaw usually sits a call or two deeper.

x-nv-dast-sinks: what the handler can reach

The second extension looks the other way, from the handler down into the code it calls. x-nv-dast-sinks is a sorted list of tags, each naming a kind of sensitive operation that static analysis found reachable from the operation's handler. The field is left out entirely when nothing was found.

TagThe handler can reach code that
sql-queryIssues a SQL statement. Always set alongside the SQL qualifiers below.
sql-read / sql-writeReads, or inserts, updates, or deletes.
sql-rawRuns statement text the API does not parameterize, including a prepared statement whose text was assembled at runtime.
sql-builderGoes through an API that binds parameters: a query builder, ORM, or prepared statement.
os-commandExecutes an operating system command.
outbound-requestMakes a request to another server.
redirectRedirects to a destination not fixed in the source.
dynamic-codeEvaluates code, or picks the code to run, at runtime.
template-renderRenders a server-side template.
file-uploadAccepts an uploaded file.
llm-query / llm-tool-callSends a prompt to a language model, or registers tools the model may call.

The naming is deliberate. A tag names the operation, not a vulnerability: a reachable SQL statement is sql-query, not "sql-injection," and an outbound fetch is outbound-request, not "ssrf." A tag named after a vulnerability would assert a finding the analysis has not made. Which vulnerability classes are worth testing on which endpoint is a decision for whoever reads the spec.

The SQL qualifiers carry the most signal. sql-raw against sql-builder is the difference between the two endpoints the contract could not tell apart. A handler that touches several call sites can carry both, since the list records which qualifiers occur somewhere in the reachable code, not how often.

One spec, read closely

To see the fields on real code, run discovery against javaspringvulny, a deliberately vulnerable Spring Boot test application. The extract takes one command:

nightvision swagger extract ./ -l java --file-format json -o openapi.json --no-upload

The spec that comes back has 38 paths and 41 operations (not counting the HEAD entries the extractor adds for GET routes). Every operation carries x-source. Thirteen carry sink tags, all of them SQL: ten include sql-raw, and three are sql-builder only.

The most instructive pair is two user searches in the same controller, with identical signatures:

GET /api/jwt/users/search/{text}       sql-builder, sql-query, sql-read
    JwtUserController.java~~36
GET /api/jwt/users/search/bad/{text}   sql-query, sql-raw, sql-read
    JwtUserController.java~~42

The first calls a Spring Data repository method, which binds its parameter. The second calls a service that concatenates the search text into a SELECT and runs it through a plain JDBC statement. That service file also contains the safe version, a prepared statement with a bound parameter, but it is commented out. The tags report only sql-raw, because the analysis follows the code that runs, not the code that was meant to.

A first review pass is one jq filter away. This lists every operation that reaches unparameterized SQL, with the line to open:

jq -r '.paths | to_entries[] | .key as $p | .value | to_entries[]
  | select(.value | type == "object")
  | select(.value["x-nv-dast-sinks"] // [] | index("sql-raw"))
  | "\(.key | ascii_upcase) \($p)  \(.value["x-source"])"' openapi.json

On this app it prints ten operations, plus HEAD entries for the GET routes. Two of them, the item searches at BasicAuthItemController.java~~23 and TokenItemController.java~~23, are the SQL injections that a runtime scan of the same app confirmed in The App Your Agent Built This Morning Has Never Been Scanned. The spec pointed at them before any request was sent. The scan is what proved them.

What the tags do not say

The tags are reachability markers. In the terms of the three evidence levels, they sit at the first and weakest: a path exists from the handler to a sink of that class. They do not say that request input reaches the sink, which parameter would carry it, or that anything is exploitable. An endpoint tagged sql-raw may build its statement entirely from constants.

The misses run the other way too. Reachability is followed through a bounded depth of calls, so a sink buried deeper in a call chain, or reached through dispatch the analyzer cannot resolve, goes untagged. Some tag classes are not yet emitted for every language. A handler whose file read and URL fetch share one API is not tagged outbound-request, to avoid tagging every file read as a network call.

That is why the extension carries a fail-open rule: an operation with no tags, or without a given tag, must be read as "not found," never as "not present." A consumer that uses the tags to decide what to test first, and still tests everything, is using them correctly. A consumer that uses them to skip endpoints has turned a hint into a blind spot.

The spec is also honest about its own guesses. An operation the analyzer could not fully establish from the code carries an x-unsound field naming what is approximate: the path, the method, the handler, the route itself, or its security requirement. Whatever it does not name can be read as established from the code.

Reading your own spec

The fields cost nothing to inspect. Run the extract on a repository with --no-upload and the spec stays on your machine. Sort the sql-raw list by how exposed each route is. Look at every os-command, outbound-request, and dynamic-code tag, since each one deserves a stated reason. Then compare the path list against the documented API: routes present in source and missing from the documentation are exactly the ones nobody has been testing.

A tag is a prior, and a prior is a place to start. The proof still comes from a request. Upload the same spec to a NightVision target and the DAST scan tests the operations it describes, fully authenticated, and returns the request and response behind each finding, with the x-source line attached on supported API scans. Reachability tells you where to look; the runtime test tells you what is true.

Frequently asked questions

What is an OpenAPI specification extension?

Any field whose name starts with x-. The OpenAPI Specification allows extensions on most objects, including operations, so tools can record information the standard fields do not cover. Tools that do not recognize an extension can simply ignore it.

How can an OpenAPI spec show which endpoints touch the database?

Only if it was generated from source by a tool that analyzes handlers. A hand-written or traffic-derived spec describes the contract, not the implementation. A source-generated spec can tag each operation with the kinds of sensitive calls reachable from its handler, such as SQL reads, writes, and unparameterized statements.

Does a sql-raw tag mean the endpoint has SQL injection?

No. It means the handler can reach a SQL call that does not bind parameters. Whether request input actually reaches that call, unneutralized, is a separate question. A runtime test that sends a payload and observes the database's behavior is what answers it.

Does a missing tag mean the endpoint is safe?

No. Tags can be missing because a sink is deeper than the analysis follows, because the language does not emit that tag yet, or because the handler could not be fully analyzed. Treat a missing tag as unknown and test the endpoint anyway.

Does the x-source line point to the vulnerable code?

It points to where the route is declared: the entry point a request comes through. The vulnerable call is often in a service or repository method the handler calls. Open the line, find the parameter, and follow it forward to the sink.

Where to read next

The vocabulary behind these tags is in Sources and Sinks, Explained. For how a file and line travel with a runtime finding, read Source-Linked DAST, and for the routes a spec surfaces that documentation missed, Shadow API Discovery. The wider map of testing methods is in What You Should Know About Application Security Testing.

See what your source says, then prove it at runtime.

NightVision generates an OpenAPI spec from your source code, then runs fully authenticated DAST against the running application and returns the request and response behind every finding.