最小構成でメモリセーフなGo製rsyncが脆弱性を回避する仕組み
2025年1月、複数のセキュリティ研究者が合計rsyncの6件のセキュリティ脆弱性を公開しました。中には任意のコード実行やファイル漏洩を許すものもあり、当然ながら私のgokrazy/rsync実装が影響を受けるのか、受けるとすればどのように影響するのか気になりました。モダンでメモリセーフなプログラミング言語であるGoで、自分なりに(互換性は保ちつつ最小構成の)rsyncを実装することで、本当に特定のクラスのセキュリティ脆弱性を根本的に排除できたのでしょうか。
この深掘り記事は2025年1月から執筆を進めていましたが、執筆過程で未公開の脆弱性がさらに見つかったため公開が遅れました。「Security Vulnerabilities」セクションでは、2025年1月のバッチと2026年5月のバッチを合わせた全12件の脆弱性を扱っています。
本番環境で(アップストリーム、Sambaの)rsyncをお使いの場合は、バージョン3.4.3以降へアップグレードしてください。
本番環境でgokrazy/rsyncをお使いの場合は、バージョンv0.3.3以降へアップグレードしてください。
セキュリティ問題の細かい詳細は飛ばして、直接次のセクションへ進んでいただいても構いません:
- Goの採用が役に立ったかどうかの結論。
- gokrazy/rsyncのような最小構成の再実装が役に立つかどうかの結論。
- OpenBSDの
openrsyncとの比較(C言語製)。 - Linuxで利用できる多層防御の仕組み。
- 結論。
背景:私自身のrsync
参考までに、2022年6月にrsyncやその使い方、仕組みについてブログで解説しています。「rsync」タグが付いたすべての記事もご覧ください。
当初自分でrsyncを書いた動機(当時はサーバーのみでしたが、現在はすべての転送方向に対応しています)は、高速なパッケージ管理を目指す私のLinuxディストリビューション研究プロジェクトであるdistriのソフトウェアパッケージを提供するためでした。これを、自宅用の小型Linux+Go製インターネットルーターであるrouter7でホストしたかったのです。router7自体は、私のGoアプライアンスプラットフォームであるgokrazy上に構築されています。
今でもこの当初の目的のために複数のgokrazy/rsyncサーバーを運用していますし、それ以外にも多くの用途で使っています。rsyncをプリミティブとして(Goプログラムにリンクして使える形で!)利用できるのは本当に便利です。
セキュリティ脆弱性
この記事では、以下のセキュリティ脆弱性を取り上げます:
- CVE-2024-12084から12088 (当初のレポート)
- CVE-2024-12747(Aleksei Gorban「loqpa」氏が個別に発見)
- CVE-2026-29518(Damien Neil氏と筆者自身が発見!またNullx3D氏も独立して発見)
- CVE-2026-43617から43620
- CVE-2026-45232
上記の脆弱性のうち最初のバッチはoss-securityメーリングリストで発表されましたが、oss-securityの要約よりも当初のレポートの方が詳細であることにご注意ください。
それ以降の脆弱性は、rsyncプロジェクトのGitHub Security Advisoriesを通じて発表されました。
2025年1月のバッチ
CVE-2024-12084: ヒープバッファオーバーフロー(9.8)
概要:
- rsyncの検証が不十分でした。ネットワークから(攻撃者が制御可能な)チェックサム長を読み取り、その長さを
MAX_DIGEST_LENと比較していました。 - しかし、rsyncのデータ構造では常に16バイトのバッファが宣言されていました:
char sum2[SUM_LENGTH] - そのため、境界チェックは実質的に無効でした。攻撃者は境界外へ書き込むことができました。
- この問題は、SHA256/SHA512チェックサム対応を追加した2022年9月のコミット
ae16850で混入しました。
不適切なチェックサム長検証の詳細を展開する(Google Securityレポートからの引用)
デーモンがチェックサムを読み取る際、2種類の異なるチェックサムが読み取られます:
- 32ビットのAdler-CRC32チェックサム
- ファイルチャンクのダイジェスト。ダイジェストのアルゴリズムはプロトコルネゴシエーションの冒頭で決定されます。該当するコードは以下のとおりです:sender.c:
s->sums = new_array(struct sum_buf, s->count); for (i = 0; i < s->count; i++) { s->sums[i].sum1 = read_int(f); read_buf(f, s->sums[i].sum2, s->s2length);最も重要な点は、
sum2フィールドがs->s2lengthバイトで埋められることです。sum2のサイズは常に16です:rsync.h#define SUM_LENGTH 16 // … struct sum_buf { OFF_T offset; /**< offset in file of this chunk */ int32 len; /**< length of chunk of file */ uint32 sum1; /**< simple checksum */ int32 chain; /**< next hash-table collision */ short flags; /**< flag bits */ char sum2[SUM_LENGTH]; /**< checksum */ };
s2lengthは攻撃者が制御可能な値で、次のスニペットが示すように最大MAX_DIGEST_LENバイトまで取り得ます:sum->s2length = protocol_version < 27 ? csum_length : (int)read_int(f); if (sum->s2length < 0 || sum->s2length > MAX_DIGEST_LEN) { rprintf(FERROR, "Invalid checksum length %d [%s]\n", sum->s2length, who_am_i()); exit_cleanup(RERR_PROTOCOL); }ここでの問題は、バイナリがどのダイジェスト対応でコンパイルされたかによって、
MAX_DIGEST_LENが16バイトより大きくなる可能性があることです:#define MD4_DIGEST_LEN 16 #define MD5_DIGEST_LEN 16 #if defined SHA512_DIGEST_LENGTH #define MAX_DIGEST_LEN SHA512_DIGEST_LENGTH #elif defined SHA256_DIGEST_LENGTH #define MAX_DIGEST_LEN SHA256_DIGEST_LENGTH #elif defined SHA_DIGEST_LENGTH #define MAX_DIGEST_LEN SHA_DIGEST_LENGTH #else #define MAX_DIGEST_LEN MD5_DIGEST_LEN /* 16 bytes */ #endif
SHA256対応は一般的で、MAX_DIGEST_LENGTHの値を64に設定します。その結果、攻撃者はsum2バッファの制限を超えて最大48バイトまで書き込むことが可能です。
アップストリームの修正:
CVE-2024-12084に対するアップストリームの修正では、sum2フィールドを動的に確保されるsum2_arrayフィールドに変更し、xfer_sum_lenの長さで確保するようにした上で、境界チェックをこの転送で使用するアルゴリズムのチェックサム長であるxfer_sum_lenに対するチェックに修正しています。
Goは防止に役立つでしょうか?
はい。Goでは境界チェックの欠落や誤りがヒープバッファオーバーフローにつながることはありません。境界外への書き込みを試みると、Goランタイムが境界チェックを行うためパニックになります。
gokrazy/rsyncではどうでしょうか?
gokrazy/rsyncでも検証が不十分でした。ただし問題は異なります。サイズの取り違えではなく、そもそもsumヘッダーの検証をまったく行っていなかったのです。おっと!
Goランタイムの境界チェックが、境界外への書き込みの試みで発動することは、コードを次のように変更してテストを実行することで確認できます:
diff --git i/types.go w/types.go
index 5601697..899fcb8 100644
--- i/types.go
+++ w/types.go
@@ -59,7 +59,7 @@ func (sh *SumHead) WriteTo(c *rsyncwire.Conn) error {
var buf rsyncwire.Buffer
buf.WriteInt32(sh.ChecksumCount)
buf.WriteInt32(sh.BlockLength)
- buf.WriteInt32(sh.ChecksumLength)
+ buf.WriteInt32(512 /*sh.checksumLength*/)
buf.WriteInt32(sh.RemainderLength)
return c.WriteString(buf.String())
}
予想どおり、Goランタイムは次のメッセージでパニックします:
panic: runtime error: slice bounds out of range [:512] with length 16
goroutine 277 [running]:
github.com/gokrazy/rsync/rsyncd.(*sendTransfer).receiveSums(0xc0000d7b68)
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/sender.go:136 +0x339
github.com/gokrazy/rsync/rsyncd.(*sendTransfer).sendFiles(0xc0000d7b68, 0xc000120820)
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/sender.go:46 +0x134
github.com/gokrazy/rsync/rsyncd.(*Server).handleConnSender(0xc000476090, {{0x95ed9b, 0x7}, {0xc000426810, 0x2a}, {0x0, 0x0, 0x0}}, {0xa2a120, 0xc0000b2ba0}, ...)
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:397 +0x26a
github.com/gokrazy/rsync/rsyncd.(*Server).HandleConn(0xc000476090, {{0x95ed9b, 0x7}, {0xc000426810, 0x2a}, {0x0, 0x0, 0x0}}, {0xa2a120, 0xc0000b2ba0}, ...)
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:351 +0x37a
github.com/gokrazy/rsync/rsyncd.(*Server).HandleDaemonConn(0xc000476090, {0x94db80?, 0xc00018a040?}, {0x7fd15838b118, 0xc000428028}, {0xa2bd90, 0xc0002303c0})
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:307 +0xdbb
github.com/gokrazy/rsync/rsyncd.(*Server).Serve.func2()
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:450 +0xaf
created by github.com/gokrazy/rsync/rsyncd.(*Server).Serve in goroutine 260
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:448 +0xd2
もちろん、サーバー全体をクラッシュさせるのは最善の失敗の仕方ではありませんので、不足していた境界チェックを追加し、パニックではなくエラーに変える修正を行いました。
CVE-2024-12085: スタック情報漏洩によるASLR回避(7.5)
概要:
前項のCVE-2024-12084と同じ検証不足のため、攻撃者は短いチェックサムを持つアルゴリズム(例:8バイトのxxhash64)を選択しておきながら、より長いチェックサム(例:9バイト)を送っていると主張することで、被害者にレスポンス内で初期化されていないスタック内容を1バイト漏洩させることができました。
スタック内容を1バイト漏洩させるだけなら無害に見えるかもしれませんが、Google Securityレポートが述べているとおりです:
最初の2つの脆弱性は、ヒープバッファオーバーフローと情報漏洩です。これらを組み合わせることで、クライアントはrsyncサーバーが動作しているマシン上で任意のコードを実行できます。クライアントに必要なのはサーバーへの匿名の読み取りアクセスだけです。
情報漏洩の詳細を展開する(Google Securityレポートからの引用)
デーモンは、クライアントから送られてきたチャンクのチェックサムを、ローカルのファイル内容と
hash_search()内で突き合わせます。関数の冒頭では、MAX_DIGEST_LENバイトのバッファがスタック上に確保されます:static void hash_search(int f, struct sum_struct *s, struct map_struct *buf, OFF_T len) { OFF_T offset, aligned_offset, end; int32 k, want_i, aligned_i, backup; char sum2[MAX_DIGEST_LEN];デーモンは次に、クライアントから送られたチェックサムを順に処理し、各チャンクについてダイジェストを生成してリモートのダイジェストと比較します:
if (!done_csum2) { map = (schar *)map_ptr(buf, offset, l); get_checksum2((char *)map, l, sum2); done_csum2 = 1; } if (memcmp(sum2, s->sums[i].sum2, s->s2length) != 0) { false_alarms++; continue; }注目すべきは、比較されるバイト数が再び
s->s2lengthバイトであることです。この場合、s->s2lengthは最大でMAX_DIGEST_LENなので、比較自体は境界外にはみ出しません。しかし、攻撃者が制御する
s->sums[i].sum2と混同してはいけないローカルのsum2バッファは、クリアされずにスタック上に置かれるため、初期化されていないスタック内容を含んでいます。悪意のあるクライアントは、ファイルの特定チャンクについて(既知の)
xxhash64チェックサムを送ることで、デーモンにスタックバッファsum2へ8バイトを書き込ませることができます。攻撃者はその上でs->s2lengthを9バイトに設定できます。そうすると、先頭の8バイトは一致し、攻撃者が制御する9バイト目が初期化されていないスタックデータの未知の値と比較されることになります。攻撃者はファイルを255チャンクに分割し、結果としてファイルのダウンロードごとに1バイトを漏洩させることができます。同じ接続内で、あるいは接続をリセットしながら、この処理を段階的に繰り返すことが可能です。
その結果、初期化されていないスタックデータのうち
MAX_DIGEST_LEN - 8バイトを漏洩させることができ、そこにはヒープオブジェクトへのポインタやスタックカナリア、ローカル変数、グローバル変数へのポインタ、リターンポインタなどが含まれる可能性があります。これらのポインタを使えばASLRを回避できます。
アップストリームの修正:
関連するアップストリームの修正は2つあります:
- 「Some checksum buffer fixes」コミットにより、攻撃者が制御する
s->s2lengthが転送時のチェックサム長を超えられなくなったため、この攻撃を防げます。 - 「prevent information leak off the stack」コミットにより、
sum2のメモリがゼロで初期化されるため、sum2経由でのスタック漏洩自体が不可能になりました。
Goは防止に役立つでしょうか?
はい。Goは設計上、すべての変数をゼロ値で初期化します。Goプログラマーは変数を明示的に初期化することを覚えておく必要がありません。
gokrazy/rsyncではどうでしょうか?
gokrazy/rsyncはこの脆弱性の影響を受けません。Goでは変数は常に初期化されるからです。
加えて、MD4以外のチェックサムの選択はプロトコルバージョン30で初めて導入されましたが、gokrazy/rsyncはプロトコルバージョン27を実装しています。
CVE-2024-12087: シンボリックリンクを使ったパストラバーサル(7.5)
説明:(Google Securityレポートからの引用)
シンボリックリンクの同期が
-lまたは-a(--archive)フラグによって有効になっている場合、悪意のあるサーバーはクライアントに宛先ディレクトリ外へ任意のファイルを書き込ませることができます。悪意のあるサーバーは、例えば次のようなファイルリストをクライアントに送ることができます:symlink -> /arbitrary/directory symlink/poc.txtシンボリックリンクはデフォルトでは絶対パスにもなり得ますし、
../../のような文字列を含むこともあります。実際には、クライアントはファイルリストを検証し、
symlink/poc.txtというエントリを見つけるとsymlinkというディレクトリを探します。見つからなければエラーになります。サーバーがsymlinkを[ディレクトリとシンボリックリンクの両方]として送った場合、[クライアント]はディレクトリエントリのみを保持するため、攻撃を成立させるにはさらに工夫が必要です。
inc_recurseモードでは、サーバーがクライアントに対して有効化できるのですが、サーバーはクライアントに複数のファイルリストを送ります。エントリの重複排除はファイルリストごとに行われます。その結果、悪意のあるサーバーはクライアントに複数のファイルリストを送ることができます。例:# file list 1: . ./symlink (directory) ./symlink/poc.txt (regular file) # file list 2: ./symlink -> /arbitrary/path (symlink)その結果、まず
symlinkディレクトリが作成され、symlink/poc.txtはファイルリスト内で有効なエントリと見なされます。続いて攻撃者はsymlinkの種別をシンボリックリンクに変更します。サーバーがクライアントに
symlink/poc.txtファイルの作成を指示すると、シンボリックリンクが辿られ、宛先ディレクトリの外にファイルが作成される可能性があります。
Goは防止に役立つでしょうか?
いいえ。この脆弱性はロジックエラーが原因です。複数のファイルリストが使われる場合、マージされたファイルリストを再検証する必要があります。
ただし、多層防御:Goのos.Rootもご覧ください。
アップストリームの修正:
CVE-2024-12087に対するアップストリームの修正では、不足していた検証が追加されています。
gokrazy/rsyncではどうでしょうか?
gokrazy/rsyncはこの脆弱性の影響を受けません。gokrazy/rsyncは増分再帰モード(--inc-recursive)を実装していないからです。
ここでのトレードオフは実装の複雑さとリソース使用量です。増分再帰モードでは、ファイルセット全体をスキャンしてから転送を開始するのではなく、ファイルセットを「ウィンドウ」単位で扱うことができます。詳しくは私のブログ記事「How does rsync work?」もご覧ください。
CVE-2024-12088: --safe-linksのバイパス(7.5)
説明:(Google Securityレポートからの引用)
--safe-linksCLIフラグは、クライアントがサーバーから受け取ったシンボリックリンクを検証するようにします。期待される動作は、シンボリックリンクのターゲットが1)宛先ディレクトリからの相対パスであること、2)決して宛先ディレクトリの外を指さないこと、の両方を満たすことです。
unsafe_symlink()関数がこれらのシンボリックリンクの検証を担っています。この関数は、宛先ディレクトリ内での位置を基準に、シンボリックリンクのターゲットのトラバーサル深度を計算します。例えば、次のシンボリックリンクは安全でないと見なされます:
{DESTINATION}/foo -> ../../宛先ディレクトリの外を指しているためです。一方、次のシンボリックリンクは宛先ディレクトリ内に収まっているため安全と見なされます:
{DESTINATION}/foo -> a/b/c/d/e/f/../../この関数は、シンボリックリンクの宛先となるパスの中に別のシンボリックリンクが含まれている可能性を考慮していないため、バイパス可能です。例えば、次の2つのシンボリックリンクを考えてみてください:
{DESTINATION}/a -> .{DESTINATION}/foo -> a/a/a/a/a/a/../../この場合、fooは実際には宛先ディレクトリの外を指すことになります。しかし、
unsafe_symlink()関数はa/がディレクトリであると想定し、シンボリックリンクは安全だと判断してしまいます。
アップストリームの修正:
CVE-2024-12088に対するアップストリームの修正では、パスの先頭を除き、パス内のどこにも../を許容しないようにすることでunsafe_symlink()をより厳格にしています。
Goは防止に役立つでしょうか?
いいえ。この脆弱性はロジックエラーが原因です。検証関数が正しくなかったのです。同じバグを私たちも実装してしまった可能性はあります。
ただし、多層防御:Goのos.Rootもご覧ください。
gokrazy/rsyncではどうでしょうか?
gokrazy/rsyncは脆弱ではありません。--safe-links機能自体がまだgokrazy/rsyncには実装されていないからです。
CVE-2024-12086: 任意ファイル漏洩(6.8)
概要:
rsyncのレシーバー(クライアントモード)は、rsyncセンダーから提供されたファイル名をサニタイズせず、宛先ツリー外のファイルを開くことを防いでいませんでした。悪意のあるセンダーは、レシーバーに宛先ツリー外の任意のファイルのチェックサムを比較させることができました。提供された1バイトのチェックサムに対するレシーバーの反応を観察することで、悪意のあるセンダーは任意のファイルを漏洩させることができます。
ファイル漏洩の詳細を展開する(Google Securityレポートからの引用)
クライアントが悪意のあるサーバーに接続すると、サーバーはクライアントマシン上の任意のファイルの内容を漏洩させることができます。
read_ndx_and_attrs()内で、クライアントはサーバーが適切なフラグを立てた場合、fnamecmpタイプとxnameをサーバーから読み取ります。sanitize_pathsフラグはクライアントが受信する際にはセットされません。if (iflags & ITEM_BASIS_TYPE_FOLLOWS) fnamecmp_type = read_byte(f_in); *type_ptr = fnamecmp_type; if (iflags & ITEM_XNAME_FOLLOWS) { if ((len = read_vstring(f_in, xname, MAXPATHLEN)) < 0) exit_cleanup(RERR_PROTOCOL); if (sanitize_paths) { /* not enabled when client receives */ sanitize_path(xname, xname, "", 0, SP_DEFAULT); len = strlen(buf); } } else { *buf = '\0'; len = -1; } *len_ptr = len;呼び出し元(
recv_files())は、サーバーから提供された値を使って、受信データを比較する対象のファイルを決定します。case FNAMECMP_FUZZY: if (file->dirname) { pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, file->dirname, xname); fnamecmp = fnamecmpbuf; } else fnamecmp = xname; break; … fd1 = do_open(fnamecmp, O_RDONLY, 0);
receive_data()内では、xnameで指定されたファイルの内容が宛先ファイルにコピーされます。これはサーバーが負のトークンを送ることで実現できます。while ((i = recv_token(f_in, &data)) != 0) { ..snip.. if (i > 0) { ..snip.. } ..snip.. if (fd != -1 && map && write_file(fd, 0, offset, map, len) != (int)len)サーバーは比較用のチェックサムを送ります。一致しなければ0が返されます。
if (fd != -1 && memcmp(file_sum1, sender_file_sum, xfer_sum_len) != 0) return 0;戻り値が0の場合、レシーバーはジェネレーターに
MSG_REDOを送ります。ジェネレーターはその後サーバーにメッセージを書き込みます。サーバーはこれを、送ったチェックサムが正しかったかどうかを判断するシグナルとして利用できます。
blengthを1から始めることで、悪意のあるサーバーは対象ファイルの内容を1バイトずつ特定していくことが可能です。
アップストリームの修正:
CVE-2024-12086に対するアップストリームの修正では、センダーが提供したパスを検証することで、宛先ツリー外のファイルを開けないようにしています。
Goは防止に役立つでしょうか?
はい。Goにはこれを防ぐためのAPIが用意されています。多層防御:Goのos.Rootをご覧ください。
gokrazy/rsyncではどうでしょうか?
gokrazy/rsyncは脆弱ではありません。ファジーマッチング機能はrsyncプロトコルバージョン29で導入されましたが、gokrazy/rsyncはプロトコルバージョン27を実装しているからです。
CVE-2024-12747: シンボリックリンクの競合状態(5.6)
説明:(Red Hat Security Advisoryからの引用)
rsyncに欠陥が見つかりました。この脆弱性は、rsyncのシンボリックリンク処理における競合状態に起因します。rsyncのデフォルトの動作は、シンボリックリンクに遭遇した際にそれをスキップすることです。攻撃者が適切なタイミングで通常ファイルをシンボリックリンクに置き換えた場合、デフォルトの動作をバイパスしてシンボリックリンクを辿らせることが可能でした。rsyncプロセスの権限によっては、攻撃者が機密情報を漏洩させ、権限昇格につながる可能性があります。
アップストリームの修正:
CVE-2024-12747に対するアップストリームの修正では、rsyncセンダー内のopen()呼び出しがO_NOFOLLOWオプションを使うように変更されています。その時点のアルゴリズムではパスがシンボリックリンクであることは想定されておらず(シンボリックリンクはreadlink(2)で処理されるはずです)、
Goは防止に役立つでしょうか?
はい。Goにはこれを防ぐためのAPIが用意されています。多層防御:Goのos.Rootをご覧ください。
gokrazy/rsyncではどうでしょうか?
gokrazy/rsyncはコミット1b1fbf6以前は脆弱でした。このコミットで、アップストリームのrsyncと同じO_NOFOLLOWによる緩和策が導入されました。
gokrazy/rsyncで問題を再現する再現手順を展開する
問題を再現するには、次の手順を実行してください:
gokrazy/rsync v0.2.7をチェックアウトします:
git clone https://github.com/gokrazy/rsync cd rsync git checkout v0.2.7次のようにコードにパッチを当てて修正を元に戻し、攻撃を実行します:
diff --git i/internal/nofollow/nofollow_unix.go w/internal/nofollow/nofollow_unix.go --- i/internal/nofollow/nofollow_unix.go +++ w/internal/nofollow/nofollow_unix.go @@ -2,8 +2,6 @@ package nofollow -import "golang.org/x/sys/unix" // Maybe resolves to unix.O_NOFOLLOW on unix systems, // 0 on other platforms. -const Maybe = unix.O_NOFOLLOW +const Maybe = 0 // unix.O_NOFOLLOW diff --git i/internal/sender/do.go w/internal/sender/do.go --- i/internal/sender/do.go +++ w/internal/sender/do.go @@ -2,6 +2,8 @@ package sender import ( "fmt" + "os" + "path/filepath" "sort" "github.com/gokrazy/rsync/internal/log" @@ -55,6 +57,15 @@ func (st *Transfer) Do(crd *rsyncwire.CountingReader, cwr *rsyncwire.CountingWri st.Logger.Printf("file list sent") } + // HACK: swap out the passwd file with a symlink to /etc/passwd + if err := os.Remove(filepath.Join(modPath, "passwd")); err != nil { + return nil, err + } + if err := os.Symlink("../passwd", filepath.Join(modPath, "passwd")); err != nil { + return nil, err + } + st.Logger.Printf("HACK: swapped passwd file for symlink") + // Sort the file list. The client sorts, so we need to sort, too (in the // same way!), otherwise our indices do not match what the client will // request.
TestReceiverSymlinkTraversalテストを実行すると、サーバーがシンボリックリンクを辿ったことがわかります:
receiver_test.go:371: unexpected file contents: diff (-want +got):
bytes.Join({
- "benign",
+ "secret",
}, "")
驚きの発見
この記事のドラフトをGoセキュリティチームのメンバーであり、トラバーサル耐性のあるos.Root APIの作者でもあるDamien Neil氏と共有したところ、彼はこう指摘しました:
CVE-2024-12747に対するgokrazyの修正は不十分だと思います。
os.OpenをO_NOFOLLOW付きで呼び出していますが、O_NOFOLLOWはパスの最後のコンポーネントにおけるシンボリックリンクのトラバーサルしか防ぎません。おそらく、パスのより前のコンポーネントを置き換えることで、
os.Open("dir/passwd")がdirを/etcへのシンボリックリンクにすることでリダイレクトされる脆弱性がまだ残っています。
私たちはこのことを2025年4月にrsyncのセキュリティ連絡先に報告しました。2025年12月に、別の方も独立してこの問題を発見し報告していたことを知りました。
最終的に、この件は2026年5月20日に公開されたCVE-2026-29518となりました。
2026年5月のバッチ
CVE-2026-29518: シンボリックリンクの競合状態(7.0)
説明:(rsync 3.4.3 NEWSエントリからの引用)
chrootなしのデーモンモードにおける、ローカルでの権限昇格を許すTOCTOUシンボリックリンク競合状態。
use chroot = noで設定されたrsyncデーモンは、親パス要素に対するtime-of-check/time-of-use競合にさらされます。モジュールへの書き込み権限を持つローカル攻撃者は、レシーバーがチェックしてからopen()するまでの間に親ディレクトリ要素をシンボリックリンクに置き換えることで、読み取り(basisファイルの漏洩)や書き込み(ファイルの上書き)をモジュール外へリダイレクトできます。デーモンが高い権限で動作している場合、これにより権限昇格が可能になります。デフォルトの
use chroot = yesでは影響を受けません。到達範囲:デーモンホスト上のローカル攻撃者、モジュールパスへの書き込み権限、
use chroot = noで設定されたデーモン。
アップストリームの修正:
CVE-2026-29518に対するアップストリームの修正では、Goのos.Root APIに似たsecure_relative_open()が使われています。
Goは防止に役立つでしょうか?
はい。Goにはこれを防ぐためのAPIが用意されています。多層防御:Goのos.Rootをご覧ください。
gokrazy/rsyncではどうでしょうか?
gokrazy/rsyncは、センダーとレシーバーをトラバーサル耐性のあるos.Root APIへ切り替えるまで脆弱でした。
CVE-2026-43618: 整数オーバーフローによるリモートメモリ漏洩(8.1)
説明:(GitHub Security Advisoryからの引用)
説明:レシーバーの圧縮トークンデコーダーが、32ビット符号付きカウンタをオーバーフローチェックなしで累積していました。悪意のあるセンダーは、巧妙に操作することでオーバーフローを引き起こし、プロセスメモリの内容(環境変数、パスワード、ヒープやライブラリのポインタなど)を攻撃者に漏洩させることができます。これによりASLRが大幅に弱体化し、さらなる攻撃が容易になります。
到達範囲:圧縮が有効な認証済みデーモン接続(両ピアが対応をアドバタイズしている場合、プロトコル>=30ではデフォルトで有効)。デーモン側で圧縮を無効化する(rsyncd.confで「refuse options = compress」)ことが回避策として利用可能です。
アップストリームの修正:
CVE-2026-43618に対するアップストリームの修正では、不足していたチェックが導入されています。
gokrazy/rsyncではどうでしょうか?
gokrazy/rsyncは圧縮を実装していないため脆弱ではありません。圧縮対応が一見単純に見えて実は容易ではない理由については、gokrazy/rsync issue #35で詳しく説明しています。
CVE-2026-43620: 範囲外読み取り後のDOS(6.5)
説明:(GitHub Security Advisoryからの引用)
send_files()にparent_ndx<0ガードを追加した2025年の修正が、見た目が同一のrecv_files()内のブロックには適用されていませんでした。悪意のあるrsyncサーバーは、互換性フラグでCF_INC_RECURSEをセットし、ソート後の最初のエントリが先頭の「.」ディレクトリではないflist(これによりrecv_file_list()がparent_ndx = -1を設定します)を送った上で、ndx=0かつITEM_TRANSFERではないiflagワードを持つ転送レコードを送ることで、接続してきた任意のクライアントを確定的にSIGSEGVさせることができます。レシーバーはdir_flist->files[-1]を読み取り、その結果をデリファレンスします。glibcのx86-64では、デリファレンスされるポインタは未マップのアドレスに配置されたmmapチャンクのメタデータであるため、きれいなSEGV_MAPERRとなります。glibc以外のメモリアロケーターについては監査されていません。到達範囲:攻撃者が制御するURLから通常のpullを行う任意のrsyncクライアント。rsync:// URLでもリモートシェル経由のpullでも有効です。
inc_recurseはプロトコル30以降のデフォルトであり、被害者側で特別なオプションは必要ありません。回避策:クライアント側で
--no-inc-recursiveを指定します。
アップストリームの修正:
CVE-2026-43620に対するアップストリームの修正では、recv_files()にもparent_ndx<0ガードが追加されています。
gokrazy/rsyncではどうでしょうか?
CVE-2024-12087と同様、gokrazy/rsyncはこの脆弱性の影響を受けません。gokrazy/rsyncは増分再帰モード(--inc-recursive)を実装していないからです。
CVE-2026-43619: さらなるシンボリックリンク競合(6.3)
説明:(GitHub Security Advisoryからの引用)
説明:レシーバーのopen()呼び出しにおけるシンボリックリンク競合に対する以前の修正(CVE-2026-29518)では、他のあらゆるパスベースのシステムコールにおける同種の競合が見落とされていました:chmod、lchown、utimes、rename、unlink、mkdir、symlink、mknod、link、rmdir、lstatです。「use chroot = no」のrsyncデーモンでは、デーモンホスト上でファイルシステムへのアクセスを持つローカル攻撃者が、レシーバーがチェックしてからこれらのシステムコールのいずれかを実行するまでの間に親ディレクトリ要素へシンボリックリンクを差し込むことで、外部のモジュール外へリダイレクトさせることができます。修正では、影響を受けるパスベースのシステムコールそれぞれを、カーネルレベルで強制されるRESOLVE_BENEATH相当の confinement の下で開かれた親dirfdを経由するようにしています(Linux 5.6以降ではopenat2、FreeBSD 13以降とmacOS 15以降ではO_RESOLVE_BENEATH、それ以外ではコンポーネントごとのO_NOFOLLOWによるウォーク)。デフォルトの「use chroot = yes」では影響を受けません。
到達範囲:デーモンホスト上のローカル攻撃者、モジュールパスへの書き込み権限、use chroot = noで設定されたデーモン。
アップストリームの修正:
CVE-2026-43619に対するアップストリームの修正では、Goのos.Rootと同様に*at系のシステムコールが使われています。
Goは防止に役立つでしょうか?
はい。Goにはこれを防ぐためのAPIが用意されています。多層防御:Goのos.Rootをご覧ください。
gokrazy/rsyncではどうでしょうか?
gokrazy/rsyncは影響を受けません。全体を通じてGoのos.Root APIを使用しているからです。
CVE-2026-43617: ホスト名/ACLバイパス(4.8)
説明:(GitHub Security Advisoryからの引用)
グローバルな
daemon chroot = /Xというrsyncd.conf設定で構成されたrsyncデーモンでは、接続してきたクライアントの逆引きDNSルックアップが、デーモンが/Xへchrootした後に実行されていました。/Xの中にglibcが名前解決に必要とするファイル(/etc/resolv.conf、/etc/nsswitch.conf、/etc/hosts、NSSサービスモジュール)が含まれていない場合、ルックアップは失敗し、接続元ホスト名は「UNKNOWN」に設定されました。そのため、ホスト名ベースの拒否ルール(「hosts deny = *.evil.example」)はマッチせず、PTRレコードを制御する攻撃者は管理者が拒否するつもりだったホスト名からでも接続できてしまいました。IPベースのACLは影響を受けません。モジュールごとのuse chroot設定はこの問題とは無関係です。到達範囲:
daemon chroot = /Xで設定されたrsyncデーモン、かつホスト名ベースのACLを使用、かつ/Xにlibcのリゾルバに必要なファイルが含まれていない場合。
アップストリームの修正:
CVE-2026-43617に対するアップストリームの修正では、DNSルックアップをプロトコル内のより早い段階へ移動しています。
gokrazy/rsyncではどうでしょうか?
gokrazy/rsyncは脆弱ではありません。ホスト名ベースのallow/denyリストではなく、IPベースのallow/denyリストのみを実装しているからです。
CVE-2026-45232: スタックの範囲外書き込み(3.1)
説明:(GitHub Security Advisoryからの引用)
rsyncクライアントのHTTP
CONNECTプロキシ対応には、establish_proxy_connection()(socket.c)における1バイトずれたスタックの範囲外書き込みがあります。CONNECTリクエストを発行した後、rsyncはプロキシからの最初のレスポンス行を1バイトずつ1024バイトのスタックバッファにcp < &buffer[sizeof buffer - 1]という境界で読み込みます。そのためループはbuffer[0..sizeof-2]にしか書き込みません。プロキシ(あるいはその手前にいる中間者)が、'\n'終端なしで最初のレスポンス行に1023バイト以上を返した場合、ループはcp == &buffer[sizeof buffer - 1]の状態で終了します。これはループが一度も書き込まなかったスロットなので、*cpには直前のsnprintf()で整形された送信時のCONNECTリクエストがスタックに残した古いバイトが入っています。ループ後のコードは次のように処理します:if (*cp != '\n') /* (*cp is uninitialised stack data) */ cp++; /* cp now &buffer[sizeof]: one past end */ *cp-- = '\0'; /* one-byte OOB write on the stack */
'\0'はスタック上のbuffer[1024]の終端を1バイト超えた位置に書き込まれ、隣接するスタックスロットにあるデータを破壊します。AddressSanitizerはsocket.c:95のestablish_proxy_connectionフレームでstack-buffer-overflowを報告します。
アップストリームの修正:
CVE-2026-45232に対するアップストリームの修正では、攻撃者が提供したデータの検証が行われています。
gokrazy/rsyncではどうでしょうか?
gokrazy/rsyncはそのようなプロキシ対応を実装していないため脆弱ではありません。
Goに関する結論
Goがどのように役立つかをまとめます:
- Goランタイムの境界チェックにより、より深刻なセキュリティ問題がパニックに置き換わります。
- パニックは依然としてサービス拒否(DoS)のリスクですが、はるかにましです。
- Goはメモリをゼロで初期化するため、CVE-2024-12085のような情報漏洩は起こり得ません。
- Goの
os.RootAPIにより、残りの脆弱性の大半を防ぐことができます。 - 12件の脆弱性のうち、Goを使っても防げなかった正真正銘のアプリケーションロジックのバグは1件(CVE-2026-43617)のみでした。
| CVE番号 | 原因 | リスク(C) | Goは役立つか |
|---|---|---|---|
| 2024-12084 | 検証不十分 | ヒープバッファオーバーフロー | ✅ 境界チェック(パニック!) |
| 2024-12085 | 検証不十分 | 情報漏洩 | ✅ ゼロ初期化 |
| 2024-12086 | 検証欠如 | 任意ファイル漏洩 | ✅ os.Root |
| 2024-12087 | 検証不十分 | 任意ファイルへの書き込み | ✅ os.Root |
| 2024-12088 | 検証不十分 | 任意シンボリックリンクの作成 | ✅ os.Root |
| 2024-12747 | TOCTOU | 特権ファイルの漏洩 | ✅ os.Root |
| 2026-29518 | TOCTOU | 特権ファイルの漏洩 | ✅ os.Root |
| 2026-43617 | 検証欠如 | 拒否リストが無効 | ❌ ロジックバグ |
| 2026-43618 | 検証不十分 | 情報漏洩 | ✅ 境界チェック(パニック!) |
| 2026-43619 | TOCTOU | 特権ファイルの漏洩 | ✅ os.Root |
| 2026-43620 | 検証不十分 | クラッシュ(DoS) | ✅ 境界チェック(パニック!) |
| 2026-45232 | 検証欠如 | メモリ書き込み | ✅ 境界チェック(パニック!) |
gokrazy/rsyncに関する結論
Goで書かれていること以外に、gokrazy/rsyncと公式のアップストリームrsyncのもう一つの大きな違いは、gokrazyの実装が最小構成であることです:
- gokrazy/rsyncは、問題の機能自体を実装していないため、多くの脆弱性の影響を受けません。例えば
--inc-recursiveなどです。 - 他のすべての互換rsync実装と同様に、gokrazy/rsyncはプロトコルバージョン27をターゲットにしています。後のプロトコルバージョンでは大幅な複雑さが導入されるからです。
- 場合によっては、実装することが望ましい機能であっても、大きな障壁を伴うことがあります。例えば圧縮は一筋縄ではいきません。詳しくはgokrazy/rsync issue #35をご覧ください。
公開時点での各CVEについて、gokrazy/rsyncが影響を受けたかどうかを見てみましょう:
| CVE番号 | 原因 | gokrazy/rsyncは実装しているか | gokrazy/rsyncは影響を受けたか |
|---|---|---|---|
| 2024-12084 | 検証不十分 | はい | ⚠️ パニック |
| 2024-12085 | 検証不十分 | いいえ(proto 30) | ✅ 脆弱性なし |
| 2024-12086 | 検証欠如 | いいえ(proto 29) | ✅ 脆弱性なし |
| 2024-12087 | 検証不十分 | いいえ(inc-rec) | ✅ 脆弱性なし |
| 2024-12088 | 検証不十分 | いいえ(safe-links) | ✅ 脆弱性なし |
| 2024-12747 | TOCTOU | はい | ❌ 脆弱性あり |
| 2026-29518 | TOCTOU | はい | ⚠️ 修正済み |
| 2026-43617 | 検証欠如 | いいえ(host deny lists) | ✅ 脆弱性なし |
| 2026-43618 | 検証不十分 | いいえ(圧縮) | ✅ 脆弱性なし |
| 2026-43619 | TOCTOU | はい | ⚠️ 修正済み |
| 2026-43620 | 検証不十分 | いいえ(inc-rec) | ✅ 脆弱性なし |
| 2026-45232 | 検証欠如 | いいえ(プロキシ) | ✅ 脆弱性なし |
念のため申し上げますと、既知の脆弱性はすべてgokrazy/rsyncで修正済みです。上記の表は、各CVEが公開された時点でどのような状態だったかを記録したものです。言い換えると:
2025年1月の脆弱性が公開された時点で、gokrazy/rsyncはパニックを起こし(CVE-2024-12084)、TOCTOU競合に対して脆弱でした(CVE-2024-12747)。TOCTOU問題を修正する過程でCVE-2026-29518を発見し、CVEが公開される前にgokrazy/rsyncで修正しました。CVE-2026-43619はさらに後で発見されましたが、同じ修正、すなわちGoのos.Rootを全面的に使用することで、こちらもgokrazy/rsyncではすでに修正済みでした。
不正確な用語
脆弱性レポートを読んでいて、レポートの言葉選びがやや誤解を招くと感じました。ほとんどのレポートは単に「サーバー」と「クライアント」とだけ書いています。しかしrsyncの転送では、rsyncクライアントとrsyncサーバーの両方が、センダー(ファイルをアップロードする側)またはレシーバー(ファイルをダウンロードする側)のいずれの役割も担うことができます!
さらに、特定のセットアップでは追加の制約があり、特定の攻撃がより困難になったり不可能になったりします。例えば、デーモンモードで動作している場合、ファイルシステムへのアクセスは事前に設定されたモジュールパスに制限できます(しかしコマンドモードではそうなりません!)。
4つの異なるセットアップと、役割/プロトコル階層の概要を示す図を以下に示します:

脆弱性レポートの文脈で言えば、任意ファイル漏洩の脆弱性(CVE-2024-12086)の当初のタイトル「Server leaks arbitrary client files」は容易に誤解を招くと言えます。
むしろ私はこう表現します。rsyncレシーバーが悪意のあるセンダーに対して任意のファイルを漏洩してしまう、と。
悪意のあるクライアントセンダーが、パッチ未適用のリモートrsyncに対して、宛先ツリー外のファイル(例えばシステムのパスワードデータベースである/etc/shadow)を開かせることができることを確認しています。例えばSSH経由のコマンドモードで動作している場合です。(ただし、デーモンモードで動作している場合、サーバーは追加のパスサニタイズを有効にするため、この攻撃は防がれます。)
同様に、シンボリックリンクのパストラバーサル脆弱性(CVE-2024-12087)では「悪意のあるサーバー」と書かれていますが、ここでも正しくは「悪意のあるセンダー」であり、クライアントにもサーバーにもなり得ます。
OpenBSDのopenrsync(C言語)との比較
OpenBSDプロジェクトはセキュリティ重視で知られていますが、openrsyncはどうでしょうか?
openrsyncは、チェックサム長を検証し、1種類のチェックサムサイズ/アルゴリズム(MD4)のみをサポートしているため、ヒープバッファオーバーフロー(CVE-2024-12084)やスタック情報漏洩(CVE-2024-12085)の脆弱性の影響を受けません。
openrsyncは、関連する機能を実装していないため、CVE-2024-12086、CVE-2024-12087、CVE-2024-12088の影響も受けません(gokrazy/rsyncと同様です)。仮に脆弱であったとしても、openrsyncの多層防御策であるOpenBSDのunveil(2)やpledge(2)によるファイルシステムアクセスの制限により、少なくともOpenBSD上で動作している限り、攻撃の成功は防がれていたでしょう。
openrsyncは、シンボリックリンク対応を実装した当初から一貫してO_NOFOLLOWを使用していたため、CVE-2024-12747の影響を受けません。しかし、O_NOFOLLOWはこの問題に対する十分な修正ではないため、openrsyncはCVE-2026-29518の影響を受けます!
上記は2025年1月のバッチの脆弱性についてですが、2026年5月のバッチについても同様で、ほとんどの機能はそもそも実装されていません。
全体として、Kristaps氏とコントリビューターの皆さん、素晴らしい仕事だと思います。検証を diligently に実装し、攻撃対象領域を制限し、多層防御策を講じることで、報告された脆弱性のほぼすべてについて影響を受けずに済んでいます。
多層防御
Linux上で多層防御のために利用できるAPIや環境にはどのようなものがあるでしょうか?
gokrazy/rsyncが対応しているものを、伝統的なものからモダンなものへと順に紹介します。
Linuxマウント名前空間
gokrazy/rsyncプロジェクトを開始してから数週間以内に、Linux上で権限を落とし、マウント/PID名前空間を使ってrsyncサーバーが扱えるファイルシステムオブジェクトを制限する対応を追加しました。
このアプローチはパストラバーサル攻撃の緩和に非常によく機能しますが、権限が必要であり、rootとして、あるいはLinuxユーザ名前空間内(お使いのディストリビューション/システムで有効な場合)で実行する必要があります。
その制約のため、マウント名前空間はサーバー用途には非常に適していますが、通常は人間のユーザーアカウントで実行される対話的な一回限りの転送では利用できないことが多いです。
systemdによる強化
Linuxのマウント/PID名前空間対応を導入したのと同じコミットで、ホームディレクトリへのファイルシステムアクセスを制限するsystemdサービスファイルも追加し、READMEでユースケースに応じてファイルシステムアクセスをさらに制限することを推奨しました。
これらのファイルシステム制限が正しく設定されていれば、ファイル漏洩(CVE-2024-12086)やパストラバーサル(CVE-2024-12087)の脆弱性を緩和できます。
シンボリックリンクの競合状態(CVE-2024-12747)はrsyncプロセスを介した権限昇格に依存しますが、DynamicUser機能のおかげで、私たちのプロセスは他のユーザーよりも少ない権限で動作しています。
マウント名前空間と同様に、これらの対策はサーバー用途には優れていますが、対話的な一回限りの用途では設定が煩雑すぎます。
Linux Landlock
Justine氏のブログ記事「Porting OpenBSD pledge() to Linux(2022)」を見かけ、LinuxにはOpenBSDのunveil(2)システムコールに似た、非特権でプロセスごとのアクセス制御を行うLandlock APIがあることを思い出しました。基本的な考え方は、プログラムが作業するディレクトリを把握した時点でunveil("/home/michael/backups", "rw");のような呼び出しを行い、それ以外のファイルシステム上の場所へのアクセスを失うというものです。
以前GoのミートアップでLandlockについて聞いたことがあり、GoでもLandlockがサポートされていることを知っていました。2022年にはgokrazyのカーネルイメージでLandlockサポートを有効にしていました。
そこで2025年3月に試してみて、ファイルシステムアクセスを制限するLandlock対応を実装しました。一見予想するより少し時間がかかり、数時間かかりました。テスト環境でLandlockを動作させる(あるいはスキップする)際にいくつかの壁にぶつかったのです。テストでは同じプロセス内で実行される多くの関数を定義していましたが、ルールセットを繰り返し追加すると、プロセスあたり16 (!) というポリシーレイヤーの上限を超えてしまったのです。
適切にセットアップできてしまえば、これは素晴らしい解決策です。これで、特権のないgokrazy/rsyncの呼び出しであっても、rsync転送を転送元(読み取り専用)や宛先ディレクトリ(読み書き可能)に制限できるようになりました!🎉
Landlockの欠点は、プロセスレベルで動作することです。つまり、Landlockポリシーにはプログラムが必要とするファイルを含める必要があります。例えばgokrazy/rsyncはユーザーIDの解決のために/etc/passwdを読み込む必要があるため、攻撃者が狙っているのが/etc/passwdファイルである場合、Landlockは役に立ちません。
Goのos.Root
2025年2月のGo 1.24リリースでは、パストラバーサルに耐性のあるos.Root APIが導入されました。Goブログ「Traversal-resistant file APIs」(Damien Neil氏、2025年3月)をご覧ください。このAPIはLandlockと比べてより細かい制御(ファイルシステム操作ごと)が可能です。
Go 1.25(2025年8月リリース)ではさらに多くのメソッドがos.Rootに追加され、ほとんどのファイルシステム操作で便利に使えるようになりました。
私はgokrazy/rsyncのすべてのファイルシステム操作をos.Rootを使うように変換しました。これはまさに適材適所です。ユーザーは入力/出力ディレクトリを設定しますが、ネットワーク経由で受け取るファイル名は信頼できません。まさにos.Rootが設計された通りの用途です!
os.Rootの利用を検討し始めた当初、一部のシステムコールはこのAPIではそもそも実行できないと思っていました。例えばmknod(2)によるデバイスノードファイルの作成などです。Damien氏はこう説明してくれました:
mknodはサポートされません。
ただ、安全なmknodを実現する方法はあります:
- os.Root.OpenFileで対象の親ディレクトリを開く、
- File.Fdでそのディレクトリのファイルディスクリプタを取得する、
- https://pkg.go.dev/golang.org/x/sys/unix#Mknodatでファイルを作成する。
実際にどのようなコードになるか気になる方は、internal/receiver/generatormknod_linux.goの15~29行目でのgokrazy/rsyncの使用例をご覧ください。
もう一つのつまずきは、mknodat(2)とは異なり、Linuxにはbind(2)しかなく、bindatが存在しないこと(Linux 7.0時点)に気づいたことでした!
幸い、Lennart Poettering氏が指摘してくれたように、bindatがなくてもパス解決をスキップするトリックがあります:
とりあえず
/proc/self/<fd>/foobarにbindすればいいでしょう…
そして実際、これはうまくいきます。既知の安全な/proc/self/<fd>の後に、パスではなくベース名(パスの最後のコンポーネント)のみを指定するため、パス解決がスキップされるのです(49~56行目をご覧ください)。
この2つのヒントのおかげで、gokrazy/rsync v0.3.1以降では全面的にos.Rootを使用しており、すべてのファイルシステムアクセスがトラバーサルセーフになっています!🥳
結論
検証不足が脆弱性を生む
TOCTOU脆弱性(CVE-2024-12747、CVE-2026-29518、CVE-2026-43619)を除けば、他のすべての脆弱性は入力検証の欠如や不備が原因であったことは興味深い点です。3件ではそもそも検証がまったく存在しませんでした。別の1件(CVE-2024-12088)では、ファイルシステムのパス解決という主題自体が十分にトリッキーで、既存の検証ではすべてのエッジケースをカバーできていませんでした。
Goに関する結論セクションでより詳しく説明しているとおり、最も価値のある構造的な修正は、境界チェック(=常時有効な検証)と、Goのos.Rootのようなデフォルトで安全なAPIを提供することです。
複雑すぎる
いくつかの脆弱性はrsyncプロトコルの進化から生じました。コードは以前は適切に十分な検証を行っていましたが、その後新機能が追加されたのです。例えば、チェックサムアルゴリズムのネゴシエーションが追加された際(プロトコルバージョン30)、検証が正しく更新されませんでした。増分再帰が追加された際(同じくプロトコルバージョン30)にも、個々のファイルリストには適切だった検証が、増分ファイルリストをマージするという新しい処理方式に合わせて更新されませんでした。
複雑さを避ければ脆弱性を避けられます!gokrazy/rsyncもopenrsyncも、12件中8件のセキュリティ脆弱性について、脆弱性のある機能自体を実装していないというだけで影響を受けずに済んでいます。
もちろん、これらの機能は過去のどこかの時点で誰かにとって価値があったからこそrsyncに追加されたのであり、もちろんソフトウェア開発を今後一切やめるべきだと言っているわけではありません。
ただ、ユースケースの複雑さに見合った、釣り合いの取れた複雑さの実装を使うことが理想だと考えています。言い換えれば、単純なユースケースには単純な実装を。全機能が必要な場合にのみ、フル機能の実装を選ぶべきです。
記事をランダムに読む