系列内の最新に上げても片方が残る — JFrog Artifactory の KEV 2件を版数で機械判定する

CISA が 2026年9月11日、JFrog Artifactory の脆弱性を2件まとめて KEV(Known Exploited Vulnerabilities・悪用が確認された脆弱性のカタログ)へ収載した。CVE-2026-42016 と CVE-2026-42018 である。是正期限はどちらも 2026年9月25日で、同じ日付が付いている。

項目 CVE-2026-42016 CVE-2026-42018
NVD 公開 2026-07-27(最終更新 2026-09-12) 2026-08-12(最終更新 2026-09-12)
NVD の状態 Analyzed Analyzed
CVSS 3.1 8.8(NVD Primary・AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) 7.5(JFrog 評価・AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)
CWE CWE-863(不正な認可) CWE-287(不適切な認証)
KEV 収載 2026-09-11(是正期限 2026-09-25) 2026-09-11(是正期限 2026-09-25)
該当範囲 7.133.11 未満(一区間) 系列ごとに5区間(本文)

中身は短く書ける。42016 はトークンの検証が署名と発行者しか見ておらず、そのトークンに与えられたスコープ(権限の範囲)を見ていないため、低い権限のトークンで本来届かない操作に到達できる。42018 は匿名アクセスを無効にしている構成でも、内部の匿名利用者トークンを未認証の呼び出し元へ返してしまうことがある。

この記事で扱うのは中身ではなく、「では何版に上げればよいのか」という一点である。ここに落とし穴がある。攻撃コードは書かない。

1. 該当範囲の「形」が2件で違う

NVD の CPE(該当製品の版数範囲)を API から読むと、2件の形がはっきり違う。

CVE-2026-42016 は一区間だけである。

1
cpe:2.3:a:jfrog:artifactory:*  versionEndExcluding = 7.133.11

「7.133.11 より前はすべて該当」という意味である。系列の区別が無い。

CVE-2026-42018 は5区間に分かれる

1
2
3
4
5
cpe:2.3:a:jfrog:artifactory:*                              endExcluding = 7.111.20
cpe:2.3:a:jfrog:artifactory:* startIncluding = 7.117.0 endExcluding = 7.117.27
cpe:2.3:a:jfrog:artifactory:* startIncluding = 7.125.0 endExcluding = 7.125.19
cpe:2.3:a:jfrog:artifactory:* startIncluding = 7.133.0 endExcluding = 7.133.28
cpe:2.3:a:jfrog:artifactory:* startIncluding = 7.146.0 endExcluding = 7.146.8

これはバックポート型の直し方である。ベンダが 7.111 / 7.117 / 7.125 / 7.133 / 7.146 の各系列に修正を配っているので、利用者は系列を変えずに上げれば塞がる。運用としては親切な形で、実際これがサポート付き製品では普通である。

問題は、42016 のほうがバックポートされていないことだ。一区間しか無いということは、「7.133.11 以降でしか直っていない」という意味になる。

2. 落とし穴 — 系列内の最新に上げると、片方だけ残る

ここから先が本題である。7.125 系を運用しているとする。普通の手順はこうなるはずだ。

  1. 自分の版は 7.125.4 である
  2. アドバイザリを見ると、7.125 系の修正版は 7.125.19 だと書いてある
  3. 7.125.19 に上げる
  4. 終わり

この手順で 42018 は塞がるが、42016 は残る。 7.125.19 は 7.133.11 より小さいので、42016 の「7.133.11 未満がすべて該当」に入ったままだからである。

そして厄介なのは、この失敗が成功に見えるところだ。42018 のアドバイザリに書かれた版数へ正しく上げており、その CVE については確かに直っている。2件が同じ日に KEV へ載って同じ期限を持っているので、「Artifactory の KEV 対応をやった」という記録も残る。残ったほうは、次に誰かが版数を突き合わせるまで見えない。

7.111 / 7.117 / 7.125 の3系列はすべてこの形になる。系列内の修正版がどれも 7.133.11 に届かないからだ。

使っている系列 42018 の系列内修正版 それで 42016 は塞がるか
7.111 系 7.111.20 塞がらない
7.117 系 7.117.27 塞がらない
7.125 系 7.125.19 塞がらない
7.133 系 7.133.28 塞がる
7.146 系 7.146.8 塞がる

つまり 7.111 / 7.117 / 7.125 を使っている環境は、系列をまたぐ更新が要る。これは「パッチを当てる」より重い作業で、検証の段取りも変わる。着手前に分かっているかどうかで計画が変わるので、ここは早く知りたい。

3. 目で追うのをやめて、データにする

上の表は7行しかないが、これを毎回人が突き合わせるのは危ない。区間の端(startIncluding は以上、endExcluding は未満)を1つ読み違えれば、答えは静かに間違う。しかも Artifactory の構成が複数あれば、その数だけ繰り返すことになる。

そこで NVD の該当範囲をそのままデータとして書き、判定は機械にやらせる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/usr/bin/env node
"use strict";
/*
* JFrog Artifactory の CVE-2026-42016 / CVE-2026-42018 を「両方とも塞ぐ」最小の版を出す。
* 該当範囲は NVD の CPE から読んだもの(2026-09-13 時点)。
* 使い方: node min-fixed-version.js 7.125.4
*/

// CVE-2026-42016: 単一区間(7.133.11 未満がすべて該当)
const CVE_42016 = [{ startIncluding: null, endExcluding: "7.133.11" }];

// CVE-2026-42018: 系列ごとに区間が分かれる
const CVE_42018 = [
{ startIncluding: null, endExcluding: "7.111.20" },
{ startIncluding: "7.117.0", endExcluding: "7.117.27" },
{ startIncluding: "7.125.0", endExcluding: "7.125.19" },
{ startIncluding: "7.133.0", endExcluding: "7.133.28" },
{ startIncluding: "7.146.0", endExcluding: "7.146.8" },
];

/** x.y.z を数値の配列に落とす。文字列比較では 7.146.10 < 7.146.8 になるのでやらない。 */
function parse(v) {
const m = String(v).trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
if (!m) throw new Error(`版数の形が x.y.z ではない: ${v}`);
return [Number(m[1]), Number(m[2]), Number(m[3])];
}

function cmp(a, b) {
const x = parse(a), y = parse(b);
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1;
return 0;
}

function affected(version, ranges) {
for (const r of ranges) {
const geStart = r.startIncluding === null || cmp(version, r.startIncluding) >= 0;
const ltEnd = cmp(version, r.endExcluding) < 0;
if (geStart && ltEnd) return r;
}
return null;
}

/** その版が属する系列(x.y)の修正版。無ければ null。 */
function branchFix(version, ranges) {
const [, minor] = parse(version);
for (const r of ranges) {
if (r.startIncluding && parse(r.startIncluding)[1] === minor) return r.endExcluding;
}
// 区間の先頭が null(それ以前すべて)の場合、その終端がその系列の修正版
const open = ranges.find((r) => r.startIncluding === null);
if (open && minor <= parse(open.endExcluding)[1]) return open.endExcluding;
return null;
}

function judge(version) {
const a16 = affected(version, CVE_42016);
const a18 = affected(version, CVE_42018);

// 両方を満たす最小版の候補は「42018 の各修正版」と「42016 の修正版」。
// そのうち 42016 にも 42018 にも当たらないものの最小を採る。
const candidates = [
...CVE_42018.map((r) => r.endExcluding),
...CVE_42016.map((r) => r.endExcluding),
];
const safe = candidates
.filter((c) => !affected(c, CVE_42016) && !affected(c, CVE_42018))
.sort(cmp);

return {
version,
"CVE-2026-42016": a16 ? "該当" : "非該当",
"CVE-2026-42018": a18 ? "該当" : "非該当",
系列内の42018修正版: branchFix(version, CVE_42018),
両方を塞ぐ最小版: safe[0] || null,
系列内で足りるか:
branchFix(version, CVE_42018) && !affected(branchFix(version, CVE_42018), CVE_42016)
? "足りる"
: "足りない(系列をまたぐ更新が要る)",
};
}

const arg = process.argv[2];
if (!arg) {
for (const v of ["7.98.5", "7.111.19", "7.117.4", "7.125.4", "7.133.12", "7.146.2", "7.146.10"])
console.log(JSON.stringify(judge(v)));
} else {
console.log(JSON.stringify(judge(arg), null, 2));
}

前提は Node.js だけである(実行は v20.17.0 で確認した)。依存は無い。ファイル名は min-fixed-version.js とする。

引数無しで走らせると、各系列の代表を一度に判定する。次が実際の出力である。

1
2
3
4
5
6
7
8
$ node min-fixed-version.js
{"version":"7.98.5","CVE-2026-42016":"該当","CVE-2026-42018":"該当","系列内の42018修正版":"7.111.20","両方を塞ぐ最小版":"7.133.28","系列内で足りるか":"足りない(系列をまたぐ更新が要る)"}
{"version":"7.111.19","CVE-2026-42016":"該当","CVE-2026-42018":"該当","系列内の42018修正版":"7.111.20","両方を塞ぐ最小版":"7.133.28","系列内で足りるか":"足りない(系列をまたぐ更新が要る)"}
{"version":"7.117.4","CVE-2026-42016":"該当","CVE-2026-42018":"該当","系列内の42018修正版":"7.117.27","両方を塞ぐ最小版":"7.133.28","系列内で足りるか":"足りない(系列をまたぐ更新が要る)"}
{"version":"7.125.4","CVE-2026-42016":"該当","CVE-2026-42018":"該当","系列内の42018修正版":"7.125.19","両方を塞ぐ最小版":"7.133.28","系列内で足りるか":"足りない(系列をまたぐ更新が要る)"}
{"version":"7.133.12","CVE-2026-42016":"非該当","CVE-2026-42018":"該当","系列内の42018修正版":"7.133.28","両方を塞ぐ最小版":"7.133.28","系列内で足りるか":"足りる"}
{"version":"7.146.2","CVE-2026-42016":"非該当","CVE-2026-42018":"該当","系列内の42018修正版":"7.146.8","両方を塞ぐ最小版":"7.133.28","系列内で足りるか":"足りる"}
{"version":"7.146.10","CVE-2026-42016":"非該当","CVE-2026-42018":"非該当","系列内の42018修正版":"7.146.8","両方を塞ぐ最小版":"7.133.28","系列内で足りるか":"足りる"}

上の4行がすべて「足りない」になっているのが、第2節で書いたことの実測である。7.98 / 7.111 / 7.117 / 7.125 のどれを使っていても、系列内の修正版では 42016 が残る。

自分の版を引数に渡すと1件だけ読みやすい形で出る。

1
2
3
4
5
6
7
8
9
$ node min-fixed-version.js 7.125.4
{
"version": "7.125.4",
"CVE-2026-42016": "該当",
"CVE-2026-42018": "該当",
"系列内の42018修正版": "7.125.19",
"両方を塞ぐ最小版": "7.133.28",
"系列内で足りるか": "足りない(系列をまたぐ更新が要る)"
}

両方を塞ぐ最小版7.133.28 になっているのは偶然ではない。候補は「42018 の各系列の修正版」と「42016 の修正版 7.133.11」だが、そのうち両方の該当範囲から外れる最小のものを選んでいる。7.133.11 は 42018 の 7.133.0 以上 7.133.28 未満 にまだ入るので落ちる。結果として 7.133.28 が残る。2件の範囲を交差させて初めて出てくる値であり、どちらか片方のアドバイザリだけを読んでいたら出てこない。

4. 版数を文字列で比べると壊れる

上のコードで cmp() をわざわざ書いたのには理由がある。版数を文字列のまま比べると間違うからだ。実際に確かめる。

1
2
3
const a = '7.146.10', b = '7.146.8';
console.log('文字列比較:', a < b ? a + ' < ' + b : a + ' >= ' + b);
console.log('sort(文字列):', JSON.stringify(['7.146.8','7.146.10','7.133.28','7.117.27'].sort()));
1
2
文字列比較: 7.146.10 < 7.146.8
sort(文字列): ["7.117.27","7.133.28","7.146.10","7.146.8"]

7.146.10 のほうが 7.146.8 より古いと判定されている。文字コードでは '1' < '8' なので、7.146.1... の時点で勝敗が決まってしまうためだ。並べ替えでも 7.146.10 が 7.146.8 の前に来ている。

これが実務で効くのは、まさに今回のような場面である。42018 の 7.146 系の修正版は 7.146.8 だが、その系列にはいずれ 7.146.10 以降が出る。そのとき文字列比較で「7.146.10 は 7.146.8 未満だから該当」と判定するスクリプトは、すでに直っている環境を該当と誤報する。逆向きの誤りも同じ理屈で起きうるので、どちらに転んでも信用できない。

対処は単純で、ドットで割って数値として比べるだけである。上のコードの parse()x.y.z の形を強制しているのも同じ理由で、7.146.8-rc1 のような想定外の形が来たら黙って通さずに例外にしている。判定器が静かに間違うより、止まったほうがよい。

5. 実際に何をするか

順番はこうなる。

  1. 自分の Artifactory の版を確認する。 管理画面か、/artifactory/api/system/version で読める
  2. 上のスクリプトに渡して 両方を塞ぐ最小版 を見る。 7.111 / 7.117 / 7.125 系なら、系列をまたぐ更新の計画が要る
  3. 是正期限は 2026年9月25日である。 KEV に載っているということは悪用が確認されているという意味なので、期限は目安ではない
  4. 更新後に侵害調査をする。 42016 は権限昇格(書き込み側)、42018 は未認証での情報取得(読み取り側)なので、見るところが違う。リポジトリへの書き込み記録に覚えの無い主体が無いか、同じ座標の成果物が差し替えられていないか、権限やパーミッションターゲットの設定に覚えの無い変更が無いか、そしてアクセスログに認証を伴わない取得が無いか

更新までの間、「匿名アクセスを切っているから大丈夫」という判断は成り立たない。 42018 はまさにその設定が効かない経路である。設定画面を見ても塞がっているように見えるので、版を上げる以外に気づく手立てが無い。現実的な手当ては、Artifactory をインターネットから直接到達できない位置に置くことだが、これは悪用の敷居を上げるだけで、欠陥そのものは残る。

6. 一般化できること

今回の形は Artifactory に限らない。同じ製品の2件が同じ日に KEV へ載っていても、該当範囲の形が同じとは限らない。 片方が系列ごとにバックポートされ、もう片方が最新系列でしか直っていない、という組み合わせは十分ありうる。

そのとき「アドバイザリに書かれた自分の系列の修正版に上げる」という手順は、1件ずつ見ているかぎり正しく、合わせると足りない。この種の誤りは、どの工程を見ても間違っていないので、レビューでも見つからない。

防ぎ方は1つで、該当範囲を文章として読むのをやめ、区間のデータとして持って交差させることである。今回のスクリプトがやっているのはそれだけで、行数も100行に満たない。

なお当サイトで扱った CVE はセキュリティ情報サイトにも一覧で置いている。

出典


系列内の最新に上げても片方が残る — JFrog Artifactory の KEV 2件を版数で機械判定する
https://blog.hashito.biz/2026/09/13/jfrog-artifactory-cve-2026-42016-42018-branch-fix-trap/
著者
hashito
作成日
2026年9月13日
著作権