認証の前に通るコマンド — Omada ゲートウェイ CVE-2026-19586 と、19機種の「この版より前」を機械で突き合わせる

TP-Link の Omada シリーズのゲートウェイに、認証が完了する前に OS コマンドが通る脆弱性が公開されている。CVE-2026-19586 である。

項目
CVE CVE-2026-19586
NVD 公開 2026-08-20(最終更新 2026-09-03 / vulnStatus: Analyzed)
CVSS 9.8(CVSS 3.1・NVD Primary)/ 9.3(CVSS 4.0・CNA Secondary)
CWE CWE-78(OS コマンドインジェクション)
KEV 2026-09-11 時点で未収載
対象 Omada ゲートウェイ 19機種のファームウェア

この記事で扱うのは3つである。①「認証前」がなぜ効くのか、②成立条件が3つあること(つまり全機体が即アウトではない)、③19機種ぶんの「この版より前」を人が目で突き合わせると必ず間違えるので、それを Node.js のスクリプトに落とす。攻撃コードは書かない。

1. 何が起きるのか

NVD の記述はこうである。

OpenVPN サーバとして動作するよう設定された Omada ゲートウェイにおいて、OpenVPN 接続の確立中にクライアントから供給されたデータの検証が不十分であることに起因する、認証前の OS コマンドインジェクションの脆弱性が確認された。

「接続の確立中」という一句がこの CVE の性格を決めている。OpenVPN の接続は、証明書や鍵で相手を確かめる前に、クライアントから送られてくる値をいくつか読む。その読んだ値が、機器の内部でコマンドの組み立てに流れ込んでいた。つまり認証を破っているのではなく、認証にたどり着く前の処理を叩いている

ここを取り違えないでほしい。これは OpenVPN 本体(openvpn パッケージ)の脆弱性ではない。Omada のファームウェア側が、OpenVPN の接続処理から受け取った値の扱いを誤っている。だから apt upgrade でも openvpn のバージョンを上げても関係がない。直すのは機器のファームウェアである。

成功したときの影響も NVD が明記している。「任意のコマンド実行が可能となり、当該機器の完全な侵害につながる可能性がある」。VPN の出入口を握られるということは、その先の内部ネットワークへの足がかりを渡すということでもある。

2. 成立条件は3つ — 全機体が即アウトではない

ここが実務上いちばん効く。NVD は成立条件を明示している。

  1. OpenVPN サーバ機能が有効になっていること
  2. VPN サービスに攻撃者が到達できること
  3. 攻撃者が OpenVPN の接続試行を開始できること

裏返すと、OpenVPN サーバ機能を使っていない機体は、この経路では狙われない。Omada ゲートウェイを PPPoE ルータとしてだけ使っていて VPN を有効にしていないなら、この CVE の優先度は下がる。まずそこを確かめるのが早い。

ただし優先度が下がるだけで、更新が不要になるわけではない。あとで VPN を有効にした瞬間に条件が揃う。

3. CVSS が 9.8 と 9.3 に割れている理由

評価が2つ出ている。割れているように見えるが、実は結論が変わっていないのがこのケースである。

評価元 CVSS 判定 ベクタ
NVD(nvd@nist.gov・Primary) 9.8 Critical CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CNA(Secondary) 9.3 Critical CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H

バージョンが違う(3.1 と 4.0)だけである。前に書いた境界装置3件の記事では、同じ CVSS 3.1 のなかで AC の1文字だけが割れて 8.1 と 9.8 に分かれる例を扱った。今回はそれとは別の型で、計算式そのものが違う

どちらも押さえている点は同じである。AV:N(ネットワーク経由)・PR:N(認証不要)・UI:N(利用者の操作不要)。数字の細かい差より、この3つが揃っていることのほうが実務上の意味が大きい。

4. 本題 — 19機種の「この版より前」を目で追うと必ず間違える

NVD の CPE 情報から起こした一覧がこれである。各行は「この版より前が対象」を意味する。

機種 対象となるファームウェア
ER605 2.4.4 未満
ER605W 2.0.4 未満
ER7206 2.3.5 未満
ER7212PC 2.4.3 未満
ER7406 1.3.4 未満
ER707-M2 1.4.4 未満
ER7412-M2 1.2.0 未満
ER8411 1.4.1 未満
ER706W 1.2.11 未満
ER706W-4G 1.2.6 未満 / 2.1.11 未満(ハードウェア版で2系統)
ER706WP-4G 1.1.11 未満
ER703WP-4G-Outdoor 1.1.7 未満
ER603WP-4G-Outdoor 1.0.2 未満
ER701-5G-Outdoor 1.0.3 未満
DR3220V-4G 1.2.0 未満
DR3650V / DR3650V-4G 1.2.0 未満
DR3150 1.0.1 未満

間違いやすい点が3つある。

  1. 基準がバラバラである。2.4.4 のものもあれば 1.0.1 のものもある。「1.x だから古い」は成り立たない
  2. 同じ型番で系統が分かれる。ER706W-4G は 1.2.6 と 2.1.11 の2つを持つ。ハードウェア版が違えばファームウェアの系列も違う
  3. 似た型番が並んでいる。ER605 と ER605W、ER706W と ER706W-4G と ER706WP-4G。1文字違いで基準が変わる

拠点が20か所あって機種が混在している、という状況で、この表を目で追って間違えないほうがおかしい。機械にやらせる。

5. 判定スクリプト

check.js として保存する。依存は無く、Node.js だけで動く(実測は v20.17.0)。

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
#!/usr/bin/env node
// CVE-2026-19586: Omada ゲートウェイの「この版より前」を機械で突き合わせる
// 一覧の出典: NVD の CPE 設定(https://nvd.nist.gov/vuln/detail/CVE-2026-19586)
"use strict";

// モデル名 -> 「この版より前が対象」(versionEndExcluding)
const FIXED = {
er605: ["2.4.4"],
er605w: ["2.0.4"],
er7206: ["2.3.5"],
er7212pc: ["2.4.3"],
er7406: ["1.3.4"],
"er707-m2": ["1.4.4"],
"er7412-m2": ["1.2.0"],
er8411: ["1.4.1"],
er706w: ["1.2.11"],
"er706w-4g": ["1.2.6", "2.1.11"], // ハードウェア版で2系統ある
"er706wp-4g": ["1.1.11"],
"er703wp-4g-outdoor": ["1.1.7"],
"er603wp-4g-outdoor": ["1.0.2"],
"er701-5g-outdoor": ["1.0.3"],
"dr3220v-4g": ["1.2.0"],
dr3650v: ["1.2.0"],
"dr3650v-4g": ["1.2.0"],
dr3150: ["1.0.1"],
};

// ドット区切りの版を数値列として比較する。文字列比較だと "2.4.10" < "2.4.4" になる
function cmp(a, b) {
const x = String(a).split(".").map(Number);
const y = String(b).split(".").map(Number);
const n = Math.max(x.length, y.length);
for (let i = 0; i < n; i++) {
const d = (x[i] ?? 0) - (y[i] ?? 0);
if (d !== 0) return d < 0 ? -1 : 1;
}
return 0;
}

function judge(model, version) {
const key = String(model).trim().toLowerCase();
const fixes = FIXED[key];
if (!fixes) return { verdict: "unknown", reason: `型番 "${model}" は対象一覧に無い` };
if (!/^\d+(\.\d+)*$/.test(String(version).trim())) {
return { verdict: "unknown", reason: `版 "${version}" を数値列として読めない` };
}
// 同じ型番に複数の系統があるときは、版が属する系統(メジャーが一致するもの)で判定する
const major = Number(String(version).split(".")[0]);
const lane = fixes.find((f) => Number(f.split(".")[0]) === major);
if (!lane) {
return { verdict: "unknown", reason: `版 ${version} に対応する系統が一覧に無い(一覧: ${fixes.join(" / ")})` };
}
return cmp(version, lane) < 0
? { verdict: "affected", fixed: lane }
: { verdict: "ok", fixed: lane };
}

const LABEL = { affected: "対象", ok: "対象外", unknown: "判定不能" };

if (require.main === module) {
const [model, version] = process.argv.slice(2);
if (!model || !version) {
console.error("usage: node check.js <model> <firmware-version>");
console.error("例: node check.js ER605 2.4.3");
process.exit(2);
}
const r = judge(model, version);
console.log(
`${model} ${version} -> ${LABEL[r.verdict]}` +
`${r.fixed ? `(修正版 ${r.fixed} 以降)` : ""}` +
`${r.reason ? `: ${r.reason}` : ""}`,
);
// 判定不能を「対象外」に倒さない。exit 2 で呼び出し側に返す
process.exit(r.verdict === "affected" ? 1 : r.verdict === "unknown" ? 2 : 0);
}

module.exports = { judge, cmp, FIXED };

実行結果である。echo " exit=$?" を挟んで終了コードも出している。

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
$ node check.js ER605 2.4.3
ER605 2.4.3 -> 対象(修正版 2.4.4 以降)
exit=1

$ node check.js ER605 2.4.4
ER605 2.4.4 -> 対象外(修正版 2.4.4 以降)
exit=0

$ node check.js ER605 2.4.10
ER605 2.4.10 -> 対象外(修正版 2.4.4 以降)
exit=0

$ node check.js ER706W-4G 1.2.5
ER706W-4G 1.2.5 -> 対象(修正版 1.2.6 以降)
exit=1

$ node check.js ER706W-4G 2.1.11
ER706W-4G 2.1.11 -> 対象外(修正版 2.1.11 以降)
exit=0

$ node check.js ER8411 1.4.0
ER8411 1.4.0 -> 対象(修正版 1.4.1 以降)
exit=1

$ node check.js ER9999 1.0.0
ER9999 1.0.0 -> 判定不能: 型番 "ER9999" は対象一覧に無い
exit=2

設計で意識した点を3つ挙げる。

5.1 バージョン比較を文字列でやらない

これは定番の落とし穴で、実際に確かめると分かりやすい。

1
2
3
4
5
const { cmp } = require("./check.js");
const cases = [["2.4.10","2.4.4"], ["2.4.4","2.4.10"], ["1.2.0","1.2"], ["2.0.4","2.0.4"]];
for (const [a, b] of cases) {
console.log(a, "vs", b, "->", cmp(a, b), "| 文字列比較なら", a < b ? -1 : a > b ? 1 : 0);
}
1
2
3
4
2.4.10 vs 2.4.4 -> 1 | 文字列比較なら -1
2.4.4 vs 2.4.10 -> -1 | 文字列比較なら 1
1.2.0 vs 1.2 -> 0 | 文字列比較なら 1
2.0.4 vs 2.0.4 -> 0 | 文字列比較なら 0

"2.4.10" < "2.4.4" は文字列としては真である。辞書順で 14 より前だからだ。この CVE の一覧には ER706W の 1.2.11 と ER706W-4G の 2.1.11 という、まさに2桁の部分が効く行がある。文字列比較で組むと、修正版を当てた機体が「対象」と判定される。危険側ではなく安全側に倒れるので気づきにくい。だからこそ質が悪い。

桁数の違う 1.2.01.2 を同値と扱うのも意図的である。機器の表示が 1.2 と省略されることがあるためだ。

5.2 判定不能を「対象外」に倒さない

型番が一覧に無いとき、版の形が読めないとき、系統が見つからないとき——いずれも unknown を返し、exit 2 で終わる。

1
if (!fixes) return { verdict: "unknown", reason: `型番 "${model}" は対象一覧に無い` };

ここで false(対象外)を返す実装にすると、タイプミスした型番が全部「安全」になる。20拠点ぶんを CSV で流し込んだとき、1行の綴り違いが静かに消える。測れなかったことと、測って問題が無かったことは別物である。呼び出し側で exit 2 を拾って、人が見るリストに積む。

5.3 同じ型番の2系統を「メジャーが一致する側」で選ぶ

ER706W-4G だけが ["1.2.6", "2.1.11"] と2つ持っている。ハードウェア版で系列が分かれているためだ。ここで「小さいほうと比べる」「大きいほうと比べる」のどちらに倒しても誤判定が出る。

1
2
const major = Number(String(version).split(".")[0]);
const lane = fixes.find((f) => Number(f.split(".")[0]) === major);

手元の版のメジャー番号と同じ系統を選ぶ。2.1.11 を入れたら 2.1.11 と比べ、1.2.5 を入れたら 1.2.6 と比べる。どちらにも当てはまらないメジャー(たとえば 3.0.0)が来たら unknown にする。

6. 複数拠点をまとめて流す

judge() をそのまま使えば、CSV でも JSON でも回せる。

1
2
3
4
5
6
7
8
9
10
11
12
const { judge } = require("./check.js");
const inventory = [
{ site: "本社", model: "ER8411", fw: "1.4.0" },
{ site: "大阪", model: "ER605", fw: "2.4.10" },
{ site: "福岡", model: "ER706W-4G", fw: "1.2.5" },
{ site: "倉庫", model: "ER7212PC", fw: "2.4.3" },
];
const LABEL = { affected: "対象", ok: "対象外", unknown: "判定不能" };
for (const row of inventory) {
const r = judge(row.model, row.fw);
console.log([row.site, row.model, row.fw, LABEL[r.verdict], r.fixed ?? r.reason].join("\t"));
}
1
2
3
4
本社	ER8411	1.4.0	対象	1.4.1
大阪 ER605 2.4.10 対象外 2.4.4
福岡 ER706W-4G 1.2.5 対象 1.2.6
倉庫 ER7212PC 2.4.3 対象外 2.4.3

最後の行に注目してほしい。ER7212PC の基準は「2.4.3 未満」なので、2.4.3 ちょうどは対象外である。「未満」と「以下」の取り違えは、表を目で追うときに最も出やすい間違いの1つで、機械に任せる価値がここにある。

7. すぐに更新できないとき

成立条件のどれかを外す。上に挙げた3条件のうち、利用者が動かせるのは1つ目と2つ目である。

  • OpenVPN サーバ機能を止める。これが最も確実である。別の VPN 方式へ切り替えるか、更新まで VPN を停止する
  • VPN ポートへの到達範囲を絞る。固定 IP からしか受けないなら、上位のファイアウォールや ISP 側で送信元を制限する。ただしOmada ゲートウェイ自身が境界である構成では、自分より外側に絞る場所が無い

更新後は痕跡を見る。認証前に任意コマンドが通る型なので、パッチ適用だけで終わりにはできない。管理者アカウントの予期しない追加、VPN 設定やポート転送に覚えのない項目、ゲートウェイ自身を発信元とする外向きの常時接続。設定のバックアップを更新前後で取り、差分を見るのが早い。

まとめ

  • CVE-2026-19586 は OpenVPN サーバとして動かしている Omada ゲートウェイでだけ成立する、認証前の OS コマンドインジェクションである
  • NVD 評価は CVSS 9.8(CNA は CVSS 4.0 で 9.3)。AV:N / PR:N / UI:N が揃っている点は両方の評価で共通である
  • OpenVPN 本体の脆弱性ではない。直すのは機器のファームウェアである
  • 対象は19機種で、修正版の下限は機種ごとに異なり、ER706W-4G は同じ型番で2系統ある。目で突き合わせない
  • バージョン比較は文字列でやらない2.4.10 < 2.4.4 が真になる
  • 判定不能を「対象外」に倒さない。綴り違いが静かに安全側へ消える

掲載中の CVE は CVE Watch にまとめている。この CVE の AWS / GCP / Azure / Linux ごとの対応状況もそちらに載せた。

出典: NVD - CVE-2026-19586(2026年9月11日確認。published 2026-08-20 / lastModified 2026-09-03 / vulnStatus Analyzed)、TP-Link Security Advisory (FAQ 5256)


認証の前に通るコマンド — Omada ゲートウェイ CVE-2026-19586 と、19機種の「この版より前」を機械で突き合わせる
https://blog.hashito.biz/2026/09/11/omada-cve-2026-19586-openvpn-preauth-command-injection/
著者
hashito
作成日
2026年9月11日
著作権