Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/releases/server-1.6.533.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ Server正式發行、成品讀回、8080部署、13語系×桌面/窄版×亮
本次 curl 三包由上述獨立官方 ADD 與 checksum 提供,不來自該舊 snapshot。
不新增 HIGH 豁免或 vendor-pending;仍須由正式成品安全 gate 確認修補結果。

第二次官方掃描已讀回三包 `.8`,未解決輸出仍列 74 筆:上述 curl
18 個 CVE/54 筆(含 3 筆 HIGH)及原本 20 筆 MEDIUM,curl 的
`FixedVersion` 欄位仍空白。正式 `.8` deb 的變更紀錄逐項列出這 18 個
CVE 的安全補丁,並與 Ubuntu USN-8927-1、curl 上游修補 commit 交叉核對。
本版以 OpenVEX `fixed` 記錄已套用的 Ubuntu backport;每項只對三個精確
`.8` PURL 生效,附官方公告、補丁與已驗簽 deb SHA-256。
gate 同步精確鎖定 69 項聲明及這 18 項修補集合,舊 `.7`、缺包、
`not_affected` 或無證據的新 CVE 都不得通過。保留 scanner 原始判斷,
不宣稱原始掃描為零,不降低 HIGH gate,仍需正式 workflow 以原 gate 重驗。

## 邊界與升級

不改註冊API、帳號權限、OIDC、MFA、HAProxy、DB、網路後端或效能設定。
Expand Down
19 changes: 16 additions & 3 deletions scripts/check-server-api-explorer-patch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2156,12 +2156,13 @@ for oidc_policy_smoke_marker in \
done

jq -e '
.["@context"] == "https://openvex.dev/ns/v0.2.0"
["CVE-2026-107928","CVE-2026-107929","CVE-2026-86409","CVE-2026-86410","CVE-2026-86412","CVE-2026-86414","CVE-2026-87862","CVE-2026-87863","CVE-2026-87864","CVE-2026-87865","CVE-2026-92386","CVE-2026-92387","CVE-2026-92388","CVE-2026-92389","CVE-2026-92390","CVE-2026-92391","CVE-2026-92392","CVE-2026-94266"] as $curl_fixed_cves
| .["@context"] == "https://openvex.dev/ns/v0.2.0"
and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.533"
and (.statements | length) == 51
and (.statements | length) == 69
and ([.statements[].vulnerability.name] | length == (unique | length))
and ([.statements[] | select(.status == "fixed") | .vulnerability.name] | sort)
== ["CVE-2024-52005","CVE-2026-12087","CVE-2026-13221","CVE-2026-18798","CVE-2026-27171","CVE-2026-56391","CVE-2026-57432","CVE-2026-57433","CVE-2026-75803","CVE-2026-8932"]
== ["CVE-2024-52005","CVE-2026-107928","CVE-2026-107929","CVE-2026-12087","CVE-2026-13221","CVE-2026-18798","CVE-2026-27171","CVE-2026-56391","CVE-2026-57432","CVE-2026-57433","CVE-2026-75803","CVE-2026-86409","CVE-2026-86410","CVE-2026-86412","CVE-2026-86414","CVE-2026-87862","CVE-2026-87863","CVE-2026-87864","CVE-2026-87865","CVE-2026-8932","CVE-2026-92386","CVE-2026-92387","CVE-2026-92388","CVE-2026-92389","CVE-2026-92390","CVE-2026-92391","CVE-2026-92392","CVE-2026-94266"]
and ([.statements[] | select(.status == "not_affected") | .vulnerability.name] | sort)
== ["CVE-2024-2236","CVE-2024-56433","CVE-2025-1352","CVE-2025-1376","CVE-2025-66382","CVE-2026-13757","CVE-2026-18374","CVE-2026-18477","CVE-2026-18508","CVE-2026-27456","CVE-2026-3184","CVE-2026-32776","CVE-2026-32777","CVE-2026-32778","CVE-2026-40228","CVE-2026-41080","CVE-2026-45186","CVE-2026-50219","CVE-2026-53910","CVE-2026-54371","CVE-2026-56131","CVE-2026-56132","CVE-2026-56392","CVE-2026-56403","CVE-2026-56404","CVE-2026-56405","CVE-2026-56406","CVE-2026-56407","CVE-2026-56408","CVE-2026-56409","CVE-2026-56410","CVE-2026-56411","CVE-2026-56412","CVE-2026-56855","CVE-2026-57062","CVE-2026-66046","CVE-2026-72522","CVE-2026-76641","CVE-2026-76957","CVE-2026-78662","GO-2026-5932"]
and ([.statements[] | select(.status == "under_investigation") | .vulnerability.name] | sort)
Expand All @@ -2170,6 +2171,18 @@ jq -e '
and all(.statements[].products[]; (.["@id"] | startswith("pkg:") and (contains("*") | not)))
and all(.statements[] | select(.status == "not_affected");
(.justification | type) == "string" and (.impact_statement | length) > 20)
and all(.statements[] | select(.vulnerability.name as $cve | $curl_fixed_cves | index($cve));
.status == "fixed"
and (.vulnerability.name as $cve | .impact_statement | contains($cve))
and ([.products[]."@id"] | sort) == ["pkg:deb/ubuntu/curl@8.18.0-1ubuntu2.8?arch=amd64&distro=ubuntu-26.04","pkg:deb/ubuntu/libcurl3t64-gnutls@8.18.0-1ubuntu2.8?arch=amd64&distro=ubuntu-26.04","pkg:deb/ubuntu/libcurl4t64@8.18.0-1ubuntu2.8?arch=amd64&distro=ubuntu-26.04"]
and (.impact_statement | contains("https://ubuntu.com/security/notices/USN-8927-1")
and contains("8.18.0-1ubuntu2.8")
and contains("debian/patches/")
and contains("https://github.com/curl/curl/commit/")
and contains("2c418a61d7165d0cde01d9cb05ae025cc41fc75ef6d3f095996bd045ba0945dc")
and contains("8db913e1fa5a694ca6ba7b828bd18c5204e6853ac07fe7ed9c6e1f94e3444d8e")
and contains("7c0b37592f83a629cc22f0c98effa176da9edbd6d77b3f38184a9d0ddc5e0266")))

' "$runtime_vex" >/dev/null

vendor_pending_fixture=$(mktemp)
Expand Down
110 changes: 110 additions & 0 deletions scripts/test-server-curl-fixed-vex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Exercise the production VEX jq predicate against exact curl fix mutations."""
import copy
import json
from pathlib import Path
import re
import subprocess
import unittest


ROOT = Path(__file__).resolve().parents[1]
GATE = (ROOT / 'scripts/check-server-api-explorer-patch.sh').read_text(encoding='utf-8')
VEX = json.loads((ROOT / 'server/security/openvex.json').read_text(encoding='utf-8'))
CURL_CVES = {
'CVE-2026-107928', 'CVE-2026-107929', 'CVE-2026-86409', 'CVE-2026-86410',
'CVE-2026-86412', 'CVE-2026-86414', 'CVE-2026-87862', 'CVE-2026-87863',
'CVE-2026-87864', 'CVE-2026-87865', 'CVE-2026-92386', 'CVE-2026-92387',
'CVE-2026-92388', 'CVE-2026-92389', 'CVE-2026-92390', 'CVE-2026-92391',
'CVE-2026-92392', 'CVE-2026-94266',
}
PURLS = {'pkg:deb/ubuntu/' + package + '@8.18.0-1ubuntu2.8?arch=amd64&distro=ubuntu-26.04'
for package in ('curl', 'libcurl3t64-gnutls', 'libcurl4t64')}
HASHES = (
'2c418a61d7165d0cde01d9cb05ae025cc41fc75ef6d3f095996bd045ba0945dc',
'8db913e1fa5a694ca6ba7b828bd18c5204e6853ac07fe7ed9c6e1f94e3444d8e',
'7c0b37592f83a629cc22f0c98effa176da9edbd6d77b3f38184a9d0ddc5e0266',
)


class CurlFixedVexTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
expressions = re.findall(r"jq -e '(\n.*?)\n' \"\$runtime_vex\" >/dev/null", GATE, re.S)
if len(expressions) != 1:
raise AssertionError('Expected the single actual production VEX predicate')
cls.expression = expressions[0]
cls.indices = [index for index, item in enumerate(VEX['statements'])
if item['vulnerability']['name'] in CURL_CVES]

def gate_accepts(self, document):
result = subprocess.run(['jq', '-e', self.expression], input=json.dumps(document),
text=True, capture_output=True, check=False)
self.assertNotIn('jq: error:', result.stderr, result.stderr)
return result.returncode == 0

def test_actual_gate_accepts_only_the_eighteen_exact_official_fixes(self):
self.assertEqual(len(self.indices), 18)
self.assertEqual(len(VEX['statements']), 69)
self.assertTrue(self.gate_accepts(VEX))
for index in self.indices:
statement = VEX['statements'][index]
self.assertEqual(statement['status'], 'fixed')
self.assertNotIn('justification', statement)
self.assertEqual({product['@id'] for product in statement['products']}, PURLS)
self.assertEqual(len(statement['products']), 3)
self.assertIn('https://ubuntu.com/security/notices/USN-8927-1', statement['impact_statement'])
self.assertIn(statement['vulnerability']['name'], statement['impact_statement'])
for digest in HASHES:
self.assertIn(digest, statement['impact_statement'])

def test_not_affected_is_rejected_for_every_new_fixed_cve(self):
for index in self.indices:
with self.subTest(cve=VEX['statements'][index]['vulnerability']['name']):
mutated = copy.deepcopy(VEX)
mutated['statements'][index]['status'] = 'not_affected'
mutated['statements'][index]['justification'] = 'vulnerable_code_not_present'
self.assertFalse(self.gate_accepts(mutated))

def test_old_versions_and_missing_packages_are_rejected_for_every_fix(self):
for index in self.indices:
for package in range(3):
for mutation in ('old-version', 'missing-package'):
with self.subTest(cve=VEX['statements'][index]['vulnerability']['name'],
package=package, mutation=mutation):
mutated = copy.deepcopy(VEX)
products = mutated['statements'][index]['products']
if mutation == 'old-version':
products[package]['@id'] = products[package]['@id'].replace('ubuntu2.8', 'ubuntu2.7')
else:
products.pop(package)
self.assertFalse(self.gate_accepts(mutated))

def test_unknown_cves_and_wildcard_products_are_rejected(self):
unknown = copy.deepcopy(VEX['statements'][self.indices[0]])
unknown['vulnerability']['name'] = 'CVE-2026-99999999'
for append in (False, True):
mutated = copy.deepcopy(VEX)
if append:
mutated['statements'].append(unknown)
else:
mutated['statements'][self.indices[0]] = unknown
self.assertFalse(self.gate_accepts(mutated))
mutated = copy.deepcopy(VEX)
mutated['statements'][self.indices[0]]['products'][0]['@id'] = 'pkg:deb/ubuntu/curl@*'
self.assertFalse(self.gate_accepts(mutated))

def test_untraceable_fix_statements_are_rejected(self):
evidence = (*HASHES, 'https://ubuntu.com/security/notices/USN-8927-1',
'https://github.com/curl/curl/commit/', 'debian/patches/')
for index in self.indices:
for marker in evidence:
with self.subTest(cve=VEX['statements'][index]['vulnerability']['name'], evidence=marker):
mutated = copy.deepcopy(VEX)
statement = mutated['statements'][index]
statement['impact_statement'] = statement['impact_statement'].replace(marker, 'REMOVED')
self.assertFalse(self.gate_accepts(mutated))


if __name__ == '__main__':
unittest.main(verbosity=2)
Loading