What Is a Vulnerability, Really? Source, Sink, and Taint

What Is a Vulnerability, Really? Source, Sink, and Taint

究竟什么是漏洞?源头、汇聚点与污点

Two Java methods. One of them will let an attacker delete your entire products table. The other is completely safe. 这里有两个 Java 方法。其中一个会让攻击者删掉你整个产品表,而另一个则是完全安全的。

public int deleteA(HttpServletRequest request, Connection conn) {
    String id = request.getParameter("id");
    String sql = "DELETE FROM products WHERE id = " + id;
    return conn.createStatement().executeUpdate(sql);
}

public int deleteB(HttpServletRequest request, Connection conn) {
    String id = request.getParameter("id");
    if (!id.matches("[0-9]+")) {
        throw new IllegalArgumentException("id must be numeric");
    }
    String sql = "DELETE FROM products WHERE id = " + id;
    return conn.createStatement().executeUpdate(sql);
}

Four lines different. If you can already see which is which and why, you know more security than most working developers. If you can’t — that’s what this article is for. By the end you’ll be able to look at almost any injection vulnerability and describe exactly what’s wrong with it, using three words. I’m building an automated vulnerability scanner, in public. Those three words are the entire foundation it’s built on. 它们之间有四行代码的差异。如果你已经能看出哪个是哪个以及原因,那么你的安全知识已经超过了大多数在职开发人员。如果看不出来,那正是这篇文章的目的。读完之后,你将能够审视几乎任何注入漏洞,并用三个词准确描述出它的问题所在。我正在公开构建一个自动化漏洞扫描器,而这三个词正是它构建的全部基石。

The three words

那三个词

Nearly every common web vulnerability has the same shape: The program takes input from the user and uses it somewhere dangerous, without cleaning it first. Three terms capture that: 几乎所有常见的 Web 漏洞都有相同的形态:程序接收用户输入,并在未经清理的情况下将其用于危险的地方。三个术语概括了这一点:

  • SOURCE (源头) — where outside input enters your program. request.getParameter("id") reads something a user typed. Since anyone can be that user, you must assume the worst possible value. SOURCE (源头) — 外部输入进入程序的地方。request.getParameter("id") 读取了用户输入的内容。由于任何人都可以是该用户,你必须假设输入的是最坏的情况。
  • SINK (汇聚点) — an operation that becomes dangerous with the wrong input. executeUpdate(sql) hands a string to your database and says “run this.” SINK (汇聚点) — 在输入错误时会变得危险的操作。executeUpdate(sql) 将字符串交给数据库并下令“执行它”。
  • TAINT (污点) — the idea that untrusted data stains everything it touches. Input arrives tainted. Copy it into a variable, that variable is tainted. Concatenate it into a bigger string, the whole string is tainted. The stain spreads. TAINT (污点) — 不受信任的数据会污染它所接触的一切。输入的数据一进来就是“带污点”的。将其复制到变量中,该变量就带了污点;将其拼接到更大的字符串中,整个字符串也就带了污点。这种污染会扩散。

The tap and the glass

水龙头与玻璃杯

Picture a tap that might be running dirty water — that’s your source. Picture a glass you’re about to drink from — that’s your sink. Taint is the dirt. A vulnerability is when dirty water flows from the tap to the glass with no filter in between. That’s it. That’s the whole model. 想象一个可能会流出脏水的水龙头——这就是你的“源头”。想象一个你准备从中饮水的玻璃杯——这就是你的“汇聚点”。污点就是脏东西。当脏水从水龙头流向玻璃杯,且中间没有任何过滤器时,漏洞就产生了。就是这样,这就是整个模型。

Now look at the two methods again: 现在再看看那两个方法:

  • deleteA — tap → glass, nothing in between. Vulnerable. deleteA — 水龙头 → 玻璃杯,中间什么都没有。存在漏洞。
  • deleteB — tap → filter → glass. The matches("[0-9]+") check rejects anything that isn’t pure digits. Safe. deleteB — 水龙头 → 过滤器 → 玻璃杯。matches("[0-9]+") 检查拒绝了任何非纯数字的内容。安全。

What the attack actually looks like

攻击的真实面貌

For deleteA, a normal request sends id=42: 对于 deleteA,正常的请求发送 id=42DELETE FROM products WHERE id = 42 Fine. One product deleted. 没问题。删除了一个产品。

Now an attacker sends id=42 OR 1=1: 现在攻击者发送 id=42 OR 1=1DELETE FROM products WHERE id = 42 OR 1=1 1=1 is true for every row. Your entire products table is gone. The attacker didn’t break into anything. They typed text into a field you gave them. Your code took that text and made it part of a command. That’s the thing worth sitting with: injection bugs aren’t about breaking in. They’re about your program treating a stranger’s text as instructions. 1=1 对每一行都成立。你整个产品表就没了。攻击者并没有“闯入”任何地方。他们只是在你提供的字段里输入了文本。你的代码接收了这些文本并将其作为命令的一部分。这一点值得深思:注入漏洞不是关于“闯入”,而是关于你的程序将陌生人的文本当作了指令来执行。

In deleteB, 42 OR 1=1 fails the [0-9]+ check and the method throws before any SQL is built. Same tap, same glass, but the filter catches the dirt. 在 deleteB 中,42 OR 1=1 未通过 [0-9]+ 检查,方法在构建任何 SQL 之前就抛出了异常。同样的水龙头,同样的玻璃杯,但过滤器拦截了脏东西。

The same shape, four different bugs

相同的形态,四种不同的漏洞

Here’s why this model is worth learning: once you see it, four of the most common vulnerability classes collapse into one idea with different taps and glasses. 这就是为什么这个模型值得学习:一旦你理解了它,四种最常见的漏洞类型都可以归结为同一个概念,只是水龙头和玻璃杯不同而已。

VulnerabilityThe dangerous sinkWhat an attacker gets
漏洞危险的汇聚点攻击者能得到什么
SQL injection (CWE-89)a database queryreads or destroys your data
SQL 注入 (CWE-89)数据库查询读取或销毁你的数据
Command injection (CWE-78)running a system commandruns any program on your server
命令注入 (CWE-78)运行系统命令在你的服务器上运行任何程序
Path traversal (CWE-22)opening a file by namereads files they shouldn’t see
路径遍历 (CWE-22)按名称打开文件读取他们本不该看到的文件
XSS (CWE-79)writing into a web pageruns code in your other users’ browsers
XSS (CWE-79)写入网页在其他用户的浏览器中运行代码

Different sinks, identical shape. Untrusted input reaches a dangerous operation with nothing neutralising it on the way. 汇聚点不同,形态完全相同。不受信任的输入到达了危险的操作,且途中没有任何中和措施。

What’s a CWE number?

什么是 CWE 编号?

The Common Weakness Enumeration is a worldwide catalogue that numbers every type of software weakness. “CWE-89” means SQL injection everywhere on earth. It matters because it lets different tools, written by different companies, talk about the same bug — which is what makes the tool comparison later in this series possible at all. “通用弱点枚举”(Common Weakness Enumeration)是一个全球性的目录,为每种类型的软件弱点进行了编号。“CWE-89”在全世界都代表 SQL 注入。这很重要,因为它让不同公司编写的不同工具能够讨论同一个漏洞——这也是本系列后续进行工具对比的前提。

Path traversal, for example, is the same story with files: 以路径遍历为例,文件操作也是同样的情况:

String name = request.getParameter("file");
File f = new File("/var/data/" + name); // sink

Send file=report.pdf and you read a report. Send file=../../../etc/passwd and you walk up out of the directory and read the system password file. Tap, glass, no filter. 发送 file=report.pdf,你读取了一份报告。发送 file=../../../etc/passwd,你就会跳出目录并读取系统密码文件。水龙头、玻璃杯,没有过滤器。

Why this is hard to automate

为什么这很难自动化

Here’s where my project starts. A scanner can trace the path — that’s mechanical. Follow the data from getParameter through every variable it touches until it reaches executeUpdate. If a path exists, flag it. That technique is called taint analysis and it’s what most security scanners do. 这就是我的项目开始的地方。扫描器可以追踪路径——这是机械化的过程。从 getParameter 开始追踪数据经过的每一个变量,直到它到达 executeUpdate。如果存在路径,就标记它。这种技术被称为污点分析,也是大多数安全扫描器所做的事情。

But run that on the two methods at the top, and it flags both. Because in deleteB, the data genuinely does flow from source to sink. The matches("[0-9]+") line doesn’t break the path — id is still the same variable, still reaching the same query. What that line changes isn’t the route, it’s the meaning: after it, id can only be digits, so the attack is impossible. 但如果对开头的那两个方法运行这种分析,它会把两个都标记出来。因为在 deleteB 中,数据确实从源头流向了汇聚点。matches("[0-9]+") 这一行并没有切断路径——id 仍然是同一个变量,仍然到达了同一个查询。这一行改变的不是路径,而是含义:在此之后,id 只能是数字,因此攻击是不可能的。

A path-tracing tool sees connectivity. It cannot see meaning. So real scanners face an ugly choice: hand-code a list of every function that counts as a filter (impossible to keep complete — every library has its own), or report the flow anyway and let developers sort it out. Most choose the second. That’s why security tools have a reputation for crying wolf. 路径追踪工具看到的是连通性,它无法理解含义。因此,真正的扫描器面临一个尴尬的选择:手动编写一份包含所有过滤器函数的列表(不可能做到完整——每个库都有自己的过滤器),或者直接报告所有流向,让开发人员自己去筛选。大多数扫描器选择了后者。这就是为什么安全工具常被诟病“狼来了”(误报率高)。

Going deeper (skip if you just want the lesson)

深入探讨(如果只想了解基础知识可跳过)

The industry’s own test suite makes this concrete. The OWASP Benchmark is a set of Java test cases with known answers, used to grade scanners. In the four categories I detect, it contains 1,478 cases: 777 real vulnerabilities and 701 that are deliberately built to look vulnerable while being safe — exactly the deleteB pattern, using real filters like numeric allow-lists, character stripping, and encoders. Forty-seven percent of the test set exists purely to punish tools that can’t tell meaning from connectivity. That ratio isn’t an accident. 行业内的测试套件让这一点变得具体。OWASP Benchmark 是一套带有已知答案的 Java 测试用例,用于评估扫描器。在我检测的四个类别中,它包含了 1,478 个案例:777 个真实漏洞,以及 701 个故意设计成看起来有漏洞但实际上安全的案例——这正是 deleteB 的模式,使用了数字白名单、字符过滤和编码器等真正的过滤器。测试集中 47% 的案例纯粹是为了惩罚那些无法区分“含义”与“连通性”的工具。这个比例并非偶然。