版本号存数据库 + 范围匹配:SemVer 的正确姿势

漏洞库 / 依赖资产表里经常要存”受影响版本范围”,然后拿具体版本号去匹配——比如 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)
);

数据:

productmin_versionmax_versionmin_includemax_include
log4j2.02.15.010
spring5.3.05.3.1810
spring5.2.05.2.2010

多个范围(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-beta91.0+20130313144700)编码复杂
  • 需要维护 encode / decode 函数

大厂漏洞平台一般是”方案 3 粗筛 + 方案 1 精确”组合——SQL 层快速缩小范围,程序层用 SemVer 库保证语义正确。

处理预发布版本

2.0-beta92.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 库:

三个方案对比

方案存储查询速度语义正确性复杂度推荐场景
存字符串一列完美规则量少(< 1 万条)
上下界拆列4 列靠程序推荐,通用最优
版本数字化长整型极快简单版本 OK亿级数据 + 版本规范

一句话总结

版本号别当字符串比——SQL 存上下界列 + 程序里跑 SemVer 库判定是最稳的组合。追求极致查询速度就把版本 encode 成整数走索引,代价是预发布版本处理复杂。