GIMP CVE-2026-59090 引き算で残量が最大値になる

画像編集ソフトの GIMP に、細工された .psd ファイルを開くだけで任意コード実行に至る脆弱性が見つかっている。CVE-2026-59090 である。

原因は 1 つの変数の引き算だ。残りバイト数を符号なし整数で持っていて、引きすぎたときに 0 を下回らず最大値へ回り込む。

この記事では、その回り込みがなぜコード実行まで届くのかを仕組みから読み解き、最後に Node.js で同じ形を再現し、検査をどこに置けば止まるのかを確かめるスクリプトを置く。GIMP を用意する必要はない。

何が起きるのか

NVD の記述を整理すると、成立の筋道はこうなる。

GIMP の PSD ファイル形式プラグインには、block_rem という変数がある。名前のとおり「このブロックの残りバイト数」を持つ変数で、符号なし整数である。

PSD の中身は、レイヤーリソースブロックが連なった構造になっている。パーサはブロックを 1 つ読むたびに、読んだぶんを block_rem から引く。残りが 0 になったら終わり、という素直な作りだ。

ここで、ファイル側が残量より大きいブロック長を書いていたらどうなるか。

符号つき整数なら結果は負になり、while (block_rem > 0) の条件が偽になって止まる。ところが符号なし整数は負の値を表現できない。 0 から 1 を引くと -1 にはならず、その型の最大値へ回り込む。32 ビットなら 4294967295 だ。

つまりパーサは、残量が尽きているのに「まだ約 43 億バイト残っている」と信じて読み続ける。

NVD はこの先をこう書いている。アンダーフローはパーサの混乱を招き、攻撃者が任意のデータをレイヤーリソースブロックとして注入できるようになる。最終的に任意コード実行に至り、被害者のシステム上で悪意あるコードを実行できる。

読み進めた先はファイルの外側、つまり確保した領域の外だ。そこを構造体として解釈させれば、書き込む先も攻撃者が選べるようになる。CWE の分類は CWE-191(整数アンダーフロー) だが、実際に効いているのはその後段である。

評価が 2 つに割れている

この CVE は、CVSS の評価が 2 つ付いている。

評価者 スコア ベクタ
NVD(主評価) 9.9 Critical AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Red Hat(二次評価) 8.4 High AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:H/A:H

差は 2 か所だけだ。

  • AC(攻撃条件の複雑さ) … NVD は L(低)、Red Hat は H(高)
  • C(機密性への影響) … NVD は H、Red Hat は L

Red Hat のほうが「成立条件は厳しく、情報漏えいの影響は限定的」と見ている。ディストリビュータの評価は自社ビルドの緩和機構(スタック保護、ASLR、コンパイラの強化オプション)を織り込んでいることが多いので、NVD の主評価より低く出るのは珍しくない。

どちらも Critical / High の帯にあるので、対応の優先度が変わるほどの差ではない。低いほうを採用して先送りする根拠にはならない。

入口は「ファイルを開くこと」

画像編集ソフトの脆弱性は軽く見られやすい。だが PSD は業務でやり取りされるファイル形式だ。メールの添付、共有ストレージ、外注から受け取った素材。開くという操作そのものが業務なので、「不審なファイルを開かない」という一般的な注意が働きにくい。

そしてもう 1 つ、見落とされやすい経路がある。GIMP はヘッドレスで動く。

1
gimp --no-interface --batch '(gimp-quit 0)'

投稿された画像を変換するパイプラインに GIMP を組み込んでいる場合、利用者の操作なしに外部由来の .psd がパーサへ届く。 コンテナイメージに GIMP を入れて ECS / Cloud Run / AKS で回している構成がこれにあたる。デスクトップ利用より優先度が高い。

PR:L(低い権限が必要)は「ローカルの一般ユーザ権限」を指すが、バッチ変換のサービスアカウントもこの条件を満たす。

実際に試す: 引き算がどこで壊れるか

ここからは手元で動かせる。Node.js 20 以降があれば依存は要らない。

JavaScript の数値は本来 64 ビット浮動小数点だが、>>> 0(符号なし右シフト 0 ビット)を通すと符号なし 32 ビットに丸められる。 これで C の uint32_t と同じ回り込みを再現できる。

underflow.js として保存する。

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
// PSD のレイヤーリソースブロック列を読む、最小のパーサ。
// 各ブロックは [4バイト: 長さ][本体] の形とする。
const subU32 = (a, b) => (a - b) >>> 0; // 符号なし32ビットの引き算

function parse(buf, { checked }) {
let off = 4;
let block_rem = buf.readUInt32BE(0); // 宣言された「残りバイト数」
const trace = [];

while (block_rem > 0) {
if (trace.length > 3) return { trace, stop: "打ち切り(4周)", block_rem };
const len = buf.readUInt32BE(off);
off += 4;
if (checked && (len + 4 > block_rem || off + len > buf.length)) {
return { trace, stop: "検査で拒否", block_rem };
}
trace.push({ off, len });
off += len;
block_rem = subU32(block_rem, 4 + len); // ← CVE-2026-59090 の形
}
return { trace, stop: "正常終了", block_rem };
}

function run(name, buf, checked) {
let r;
try {
r = parse(buf, { checked });
} catch (e) {
r = { trace: [], stop: "Buffer の境界検査で例外: " + e.code, block_rem: "-" };
}
console.log(
name.padEnd(15),
(checked ? "検査あり" : "検査なし").padEnd(10),
("block_rem=" + r.block_rem).padEnd(22),
("読んだブロック=" + r.trace.length).padEnd(18),
r.stop
);
}

// 正常: 残り8バイト = [len=4][4バイト]
const ok = Buffer.alloc(24);
ok.writeUInt32BE(8, 0);
ok.writeUInt32BE(4, 4);

// 細工: 残りは8と宣言しつつ、ブロック長に16を書く
const evil = Buffer.alloc(24);
evil.writeUInt32BE(8, 0);
evil.writeUInt32BE(16, 4);

for (const [name, buf] of [["正常な .psd", ok], ["細工した .psd", evil]]) {
for (const checked of [false, true]) run(name, buf, checked);
}

console.log("\nblock_rem = 8 のときに 4 + 16 = 20 を引くと:");
console.log(" 符号つき (8 - 20) =", 8 - 20);
console.log(" 符号なし subU32(8, 20) =", subU32(8, 20));

実行する。

1
node underflow.js

出力はこうなる。

1
2
3
4
5
6
7
8
正常な .psd        検査なし       block_rem=0            読んだブロック=1          正常終了
正常な .psd 検査あり block_rem=0 読んだブロック=1 正常終了
細工した .psd 検査なし block_rem=- 読んだブロック=0 Buffer の境界検査で例外: ERR_OUT_OF_RANGE
細工した .psd 検査あり block_rem=8 読んだブロック=0 検査で拒否

block_rem = 8 のときに 4 + 16 = 20 を引くと:
符号つき (8 - 20) = -12
符号なし subU32(8, 20) = 4294967284

この出力から読み取れることが 3 つある。

1 つ目。最後の 2 行が本体だ。 同じ引き算が、符号つきなら -12、符号なしなら 4294967284 になる。while (block_rem > 0) という条件は、前者では偽、後者では真である。ループを止める条件が、型の選択ひとつで機能しなくなる。

2 つ目。「検査なし」の行が例外で止まっているのは、Node がバッファの境界を検査しているからにすぎない。 readUInt32BE は範囲外のオフセットを渡されると ERR_OUT_OF_RANGE を投げる。

C にはこの検査が無い。 同じコードを C で書けば、例外は起きず、確保領域の外を静かに読み進める。読んだ値をまた長さとして使うので、そこから先はファイルの中身ではなくメモリの中身が制御に混ざり始める。Node が例外で止まったこの位置が、C では「攻撃が始まる位置」である。 ランタイムに助けられているだけで、論理の誤りは同じだ。

3 つ目。「検査あり」の行では block_rem が 8 のまま止まっている。 引く前に弾いているので、そもそも回り込みが起きない。

検査の中身はこの 2 条件である。

1
if (len + 4 > block_rem || off + len > buf.length) reject();

前半が宣言された残量との整合、後半が実バッファ長との整合だ。片方だけでは足りない。前半だけなら、ファイル全体より大きい block_rem を先頭に書かれた時点で通ってしまう。後半だけなら、バッファ内には収まるが構造としては壊れている入力を受け入れてしまう。

そして位置が重要だ。引いたあとに block_rem を検査しても遅い。 回り込んだ値は「巨大な正の数」なので、どんな > 0 の検査も通過する。壊れたことを壊れた値で検出しようとしても無理で、引く前に、引いてよいかを確かめるしかない。

対応

対応は GIMP の更新である。

  • Red Hat Enterprise Linux 7 / 8 / 9 … RHSA-2026:61587 に対応するパッケージを適用する
  • その他のディストリ … 各セキュリティトラッカーで gimp の修正版を確認する
  • Flatpak / Snap で入れている場合 … ディストリのパッケージ更新では上がらない。flatpak update / snap refresh を別途実行する

NVD の適用範囲には GIMP 3.3.1 と RHEL 7 / 8 / 9 が並んでいる(2026-09-01 時点)。

更新までのつなぎとしては、PSD の取り扱いを絞る手がある。この欠陥は PSD プラグインのパーサに閉じているので、GIMP で .psd を開かなければ成立しない。 バッチ処理側で拡張子とマジックバイトによる PSD の除外を入れる、外部から受け取った PSD は使い捨てのコンテナで開く、といった運用が橋渡しになる。緩和であって修正ではない。

Flatpak / Snap のサンドボックスは、成立後の被害の広がりを抑える方向には働く。ただし成立そのものは防がないので、更新の代わりにはならない。

CISA の KEV カタログには収載されていない(カタログ版 2026.08.31 で確認)。ただし PR:L かつ UI:N なので、KEV に載っていないことを安全の根拠にはできない。

まとめ

  • 原因は block_rem(残りバイト数)の符号なし整数アンダーフロー。引きすぎると 0 を下回らず最大値へ回り込み、パーサが読み進める
  • ループを止める条件が、型の選択ひとつで機能しなくなる。 引いたあとの検査では遅い。引く前に、引いてよいかを確かめる
  • 検査は「宣言された残量との整合」と「実バッファ長との整合」の両方が要る
  • デスクトップより、画像変換パイプラインに GIMP を組み込んでいる構成のほうが優先度が高い。利用者の操作なしに外部由来の PSD がパーサへ届く

この CVE の対応状況(AWS / GCP / Azure / Linux 別)は CVE Watch にまとめてある。

出典は NVD の CVE-2026-59090(2026-08-10 公開・2026-08-31 最終更新・vulnStatus は Modified)、Red Hat の RHSA-2026:61587 と Bugzilla 2496584、GNOME GitLab の work item 16509 である。本文中の出力例は、上記スクリプトを Node.js 20.17.0 で実際に実行した結果をそのまま貼っている。


GIMP CVE-2026-59090 引き算で残量が最大値になる
https://blog.hashito.biz/2026/09/01/gimp-cve-2026-59090-unsigned-integer-underflow-psd/
著者
hashito
作成日
2026年9月1日
著作権