漏洞库 / 依赖资产表里经常要存”受影响版本范围”,然后拿具体版本号去匹配——比如 Log4j >= 2.0-beta9 < 2.15.0、Spring 4Shell >= 5.3.0 < 5.3.18 || >= 5.2.0 < 5.2.20。直接 SQL 字符串比较必翻车:
SELECT * WHERE version >= '2.9.0' AND version < '2.15.0';
-- 会把 2.10.0 判成 < 2.9.0(字符串按字典序)
字典序里 '2.10' < '2.9'(因为 '1' < '9')。要按语义化版本比,得单独设计。
方案 1:存字符串,程序判断(灵活但慢)
数据库里就存原始范围:
CREATE TABLE vuln_rule (
id BIGINT PRIMARY KEY,
product VARCHAR(64),
affected_range VARCHAR(255), -- '>= 2.0 < 2.15.0'
fixed_version VARCHAR(50)
);
匹配时程序里跑 SemVer 库:
Java:
import com.vdurmont.semver4j.Requirement;
import com.vdurmont.semver4j.Semver;
Requirement req = Requirement.buildNPM(">=2.0 <2.15.0");
Semver ver = new Semver("2.14.1", Semver.SemverType.NPM);
boolean vulnerable = req.isSatisfiedBy(ver);
Node.js:
import semver from "semver";
semver.satisfies("2.14.1", ">=2.0 <2.15.0"); // true
Python:
from packaging.specifiers import SpecifierSet
from packaging.version import Version
Version("2.14.1") in SpecifierSet(">=2.0,<2.15.0") # True
优点:范围表达最灵活,各种 >=、<、!=、~、^ 都支持。
缺点:数据库查不了范围——每条规则拿出来在程序里跑一遍,几万条规则会慢。
方案 2:拆上下界(企业最常用)
把 >=2.0 <2.15.0 拆成四列:
CREATE TABLE vuln_rule (
id BIGINT PRIMARY KEY,
product VARCHAR(64),
min_version VARCHAR(50),
max_version VARCHAR(50),
min_include TINYINT DEFAULT 1, -- 1: >= 0: >
max_include TINYINT DEFAULT 0, -- 1: <= 0: <
INDEX idx_product (product)
);
数据:
| product | min_version | max_version | min_include | max_include |
|---|---|---|---|---|
| log4j | 2.0 | 2.15.0 | 1 | 0 |
| spring | 5.3.0 | 5.3.18 | 1 | 0 |
| spring | 5.2.0 | 5.2.20 | 1 | 0 |
多个范围(OR 关系)拆成多行。
匹配:数据库先粗筛(按 product),程序做精确 SemVer 比:
List<VulnRule> rules = mapper.findByProduct("spring");
for (VulnRule r : rules) {
if (semver.gteq(target, r.minVersion) &&
semver.lt (target, r.maxVersion)) {
// 命中
}
}
优点:结构清晰、粗筛快、精确匹配用程序保证正确。大多数漏洞平台走这条路。
方案 3:版本数字化后 SQL 直接查
把版本号编码成一个数字,SQL 就能直接范围查:
2.15.0 → 002 015 000 → 2015000
2.14.10 → 002 014 010 → 2014010
2.10.0 → 002 010 000 → 2010000
每段用固定位数(3 位)保留:
long encode(String v) {
String[] p = v.split("\\.");
long n = 0;
for (int i = 0; i < 3; i++) {
int part = i < p.length ? Integer.parseInt(p[i].split("[^0-9]")[0]) : 0;
n = n * 1000 + part;
}
return n;
}
数据库:
CREATE TABLE vuln_rule (
id BIGINT PRIMARY KEY,
product VARCHAR(64),
min_version_int BIGINT,
max_version_int BIGINT,
min_include TINYINT,
max_include TINYINT,
INDEX idx_product_range (product, min_version_int, max_version_int)
);
查询:
SELECT * FROM vuln_rule
WHERE product = 'log4j'
AND 2014001 >= min_version_int -- target 版本 encode 后
AND 2014001 < max_version_int;
优点:纯 SQL 就能查,走索引,几百万数据也很快。
缺点:
- 每段有位数上限(3 位 = 999,遇到
10.1000.0就崩) - 预发布版本、build metadata(
2.0-beta9、1.0+20130313144700)编码复杂 - 需要维护 encode / decode 函数
大厂漏洞平台一般是”方案 3 粗筛 + 方案 1 精确”组合——SQL 层快速缩小范围,程序层用 SemVer 库保证语义正确。
处理预发布版本
2.0-beta9、2.0-rc1 这种版本比较:
- SemVer 规定
1.0.0-alpha < 1.0.0-beta < 1.0.0-rc < 1.0.0 - 字符串比又反过来:
'1.0.0' < '1.0.0-alpha'(长度)
别自己写比较,用 SemVer 库:
- Java:semver4j
- Node:semver
- Python:packaging 或 python-semver
- Go:semver
三个方案对比
| 方案 | 存储 | 查询速度 | 语义正确性 | 复杂度 | 推荐场景 |
|---|---|---|---|---|---|
| 存字符串 | 一列 | 慢 | 完美 | 低 | 规则量少(< 1 万条) |
| 上下界拆列 | 4 列 | 中 | 靠程序 | 中 | 推荐,通用最优 |
| 版本数字化 | 长整型 | 极快 | 简单版本 OK | 高 | 亿级数据 + 版本规范 |
一句话总结
版本号别当字符串比——SQL 存上下界列 + 程序里跑 SemVer 库判定是最稳的组合。追求极致查询速度就把版本 encode 成整数走索引,代价是预发布版本处理复杂。
