Sources and Sinks in Security Explained

Sources and Sinks in Security Explained

Almost every taint tool starts by learning two lists: where untrusted data enters your app, and where that data becomes dangerous. Those two lists are the sources and sinks of your program, and the words are the core vocabulary of every injection bug. Get them right and the rest of the analysis has a chance; get them wrong and whole classes of bugs walk straight past the scanner.

What sources and sinks actually mean

The vocabulary is small and worth learning exactly, because people use these words loosely and then talk past each other.

  • A source is any place attacker controlled data enters the program. A query parameter, a request header, a cookie, an uploaded file, a message off a queue, an environment variable set by an untrusted caller. If a stranger can influence the value, it is a source.
  • A sink is a place where data is dangerous if it is attacker controlled. A SQL query, a shell command, an HTML response, a file path, an HTTP redirect target. The call itself is not a bug; it becomes one when tainted data reaches it.
  • A sanitizer is code that cleans a value so it is safe for a specific sink. Parameter binding for SQL, HTML escaping for markup, an allow list for a file path. A sanitizer turns tainted data back into trusted data, but only for the sink it was built for.

The whole model is one sentence. If data from a source reaches a sink without passing a sanitizer that fits that sink, you likely have a bug. Everything a taint engine does is bookkeeping around that sentence.

The same value, from a source to a sink

The clearest way to see it is to follow one value. In each example below, the same user string starts at a source and ends at a sink with no cleaning in between. The bug class changes, but the shape does not.

SQL injection

The source is the name parameter. The sink is the database driver call.

name = request.args.get("name")            # source
query = "SELECT * FROM users WHERE name = '" + name + "'"
db.execute(query)                          # sink

A request of ?name=' OR '1'='1 changes the meaning of the query. The fix is a sink specific sanitizer: bind the value as a parameter so the driver treats it as data, never as SQL.

db.execute("SELECT * FROM users WHERE name = ?", [name])

Cross site scripting

Same source idea, different sink. Here the sink is the HTML response.

comment = request.form["comment"]          # source
response.write("<div>" + comment + "</div>")  # sink

If comment is <script>steal()</script>, the browser runs it. The sanitizer for this sink is HTML escaping, which turns < into &lt; so the tag renders as text instead of code.

Command injection

Now the sink is a shell.

host = request.args.get("host")            # source
os.system("ping -c 1 " + host)             # sink

A value of 8.8.8.8; rm -rf / runs a second command. The right sanitizer here is not escaping at all. It is passing arguments as a list so no shell parses them, or validating against a strict allow list.

subprocess.run(["ping", "-c", "1", host])

Path traversal

The sink is a file path.

fname = request.args.get("file")           # source
open("/var/data/" + fname)                 # sink

A value of ../../etc/passwd escapes the intended folder. The sanitizer resolves the final path and checks it still sits inside /var/data, rejecting anything that climbs out.

A sink is only dangerous in one context

This is the part people miss, and it is why a sanitizer is sink specific. Cleaning is not a single switch you flip on a value. It is defined relative to the sink you are heading into.

Take a value that has been HTML escaped so it is safe to drop into a page. Feed that same escaped value into a shell command and it is still dangerous, because shell metacharacters are not the ones HTML cares about. Escaping < and > does nothing about ; or | or backticks. The reverse is just as true: a value shell quoted for a command is not safe to write into HTML.

Safe is never a property of a value on its own. It is a property of a value with respect to one sink. There is no such thing as data that is clean everywhere.

Concrete case. Suppose a username is escaped for HTML on the way into a profile page, and that works. Later a developer reuses the same username to name a log file. The HTML escaping did nothing for the file path sink, and ../../ in a username now writes outside the log folder. One value, two sinks, one sanitizer that only covered the first. The mismatch is the bug.

Why sources and sinks are the first step of any taint tool

Before a tool can trace anything, it has to know what to trace from and what to trace to. That catalog of sources and sinks is the starting configuration of every taint engine, whether it reads source code statically or watches the app at run time. The propagation in the middle, how the tool carries taint from one to the other across variables and function calls, is a separate topic. We cover the tracing itself in source to sink dataflow analysis and the harder cross function version in interprocedural taint analysis. This post is only about the two endpoints.

The catalog matters because it sets the ceiling on what can ever be found. A tool cannot report a flow it was never told to look for.

  • If a source is missing, data from it looks trusted, and every bug that starts there is invisible.
  • If a sink is missing, data can pour into it and nothing flags the flow, because the tool does not know that call is dangerous.
  • If a sink is miscategorized, for example a template render function labeled as a plain string call, the tool applies the wrong rules and misses the injection.

Here is why that is worse than a single missed bug. A missing sink is not one false negative, it is a whole class of them. Say your framework adds a new way to build raw HTML and the sink list never learns about it. Every use of that call across the entire codebase, past and future, is now a blind spot. One line in a catalog decides whether a hundred real flows are seen or silently dropped. That is why serious tools treat the sink list as a living thing, and why a well modeled graph of the code, like a code property graph, is built to make sources and sinks easy to enumerate and keep current.

Putting the vocabulary to work

When you read a finding, or write a check, or argue about whether something is exploitable, name the three parts out loud. Where is the source. Where is the sink. Is there a sanitizer between them, and does it fit that sink. Most injection disputes dissolve once those three questions are answered plainly, because the argument was usually about a sanitizer that fit the wrong context.

Cataloging sources and sinks is where every scanner begins, but a fixed list only finds the bug shapes it was told about. Testing whether an app’s real assumptions hold, including sinks nobody wrote down, is the kind of work an autonomous researcher is built for, and you can read how we think about it on our about page. For more on the gap between pattern matching and reasoning about an app, see scanners vs research.

Frequently asked questions

What is the difference between a source and a sink?

A source is any place attacker controlled data enters the program, such as a query parameter, a request header, a cookie, or an uploaded file. A sink is a place where that data is dangerous if attacker controlled, such as a SQL query, a shell command, an HTML response, or a file path. Injection bugs happen when data flows from a source to a sink without a sanitizer that fits that sink.

What is a sanitizer and why is it sink specific?

A sanitizer is code that cleans a value so it is safe for one specific sink, like parameter binding for SQL or HTML escaping for markup. It is sink specific because safe is a property of a value with respect to a single sink, not a property of the value on its own. A value HTML escaped for a page is still dangerous if you pass it to a shell, because shell metacharacters are not the ones HTML escaping handles.

Why do taint tools start by listing sources and sinks?

A tool cannot trace a flow until it knows what to trace from and what to trace to, so the catalog of sources and sinks is the starting configuration of every taint engine. That list sets the ceiling on what can ever be found. The propagation in the middle, carrying taint across variables and function calls, is a separate step.

What happens if a sink is missing or miscategorized?

A missing sink is not one missed bug, it is a whole class of them. If the tool does not know a call is dangerous, data can flow into it across the entire codebase and nothing is flagged. A miscategorized sink, such as a template render function labeled as a plain string call, makes the tool apply the wrong rules and miss the injection.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.