LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

社内リポジトリにおけるGitHub Actionsの脆弱性レビュー(インターンレポート)

早稲田大学基幹理工学研究科 修士1年の斧田洋人です。このたび、LINEヤフーのインターンシップにセキュリティエンジニアとして参加し、1か月間就業しました。このブログでは、インターン中に取り組んだ内容を紹介します。

SAT1について

私が所属したセキュリティアセスメント1チーム(SAT1)は、LINEのサービスを主に担当し、次のような業務を行っています。

1. セキュリティアセスメント(SA)

リリース前の検査として、社内で開発されたソフトウェアに脆弱性が存在しないかを確認します。ソースコードの解析やアプリケーションの実行によって問題の有無を調査します。

2. バグバウンティ対応

セキュリティアセスメント1チームではLINE株式会社時代からバグバウンティプログラムを運営しており、合併後もLINEのサービスを対象に運営しています。外部から報告された脆弱性情報について、内容の評価、修正の確認、報酬額の評価などを担当します。

3. 効率的なSAのためのシステム開発

セキュリティチェックやマネジメントを自動化するプラットフォーム「SIMS (Security Issue Management System)」の開発を行っています。あわせて、LLMを活用したSA業務自動化システムの開発等にも取り組んでいます。

GitHub Actionsで起こる脆弱性調査

インターンではまず、いくつかのSA業務を経験し、実際のアプリケーションセキュリティ診断の流れを学びました。1週間で3つほどのアプリケーションを見ていく中で、「腰を据えて1つのテーマで脆弱性調査をやってみたい」という気持ちが強くなりました。

アプローチとしては、大規模サービスを時間をかけて診断するか、特定の脆弱性に注目して社内のリポジトリから探し出すかの二択がありました。そこで、以前から興味があったGitHub Actionsの脆弱性に焦点を当てることにしました。

GitHub Actionsとは

GitHub ActionsはGitHubが提供するCI/CDプラットフォームです。pushやpull_requestなどのイベントに反応し、事前に設定したワークフローに基づいてビルドやテスト、リポジトリの操作を自動実行します。

反復作業を自動化できる便利なサービスである一方、テストプロセスで信頼できないコードを自動実行する可能性や、リポジトリへの操作権限を持つことから、使い方を誤るとリスクにつながります。

事例

実際にGitHub Actionsが攻撃連鎖の起点や導線として悪用された例を2つ紹介します。

Ultralytics(PyPI/暗号資産マイナー)

人気AIライブラリのUltralyticsでは、リリース工程でGitHub Actionsのフローが不正に利用され、暗号資産マイナーを実行する悪意のあるパッケージがPyPIに公開されました。

公開後まもなく除去されたものの、一部環境でマイニングが実行された形跡が報告されています。

参考: Compromised ultralytics PyPI package delivers crypto coinminer

Nx(npm/installで認証情報窃取)

ビルドツールのNxでは、GitHub Actionsワークフローへの注入が連鎖の入口となり、ワークフローの変更から機密トークンの取得、悪性バージョンのnpm公開へと至りました。インストール時にウォレットやAPIキーを収集し、被害者のGitHubリポジトリへ流出させる挙動が確認されています。

少なくとも約1,400アカウントで流出用のリポジトリが作られたと報告されており、プライベートリポジトリが公開化される例も確認されています。

参考: Security Alert | NX Compromised to Steal Wallets and Credentials

社内リポジトリの調査

こうした実例を踏まえ、社内のリポジトリに同様の攻撃につながる脆弱なワークフローがないかを調査しました。ここでは、レビュー中に確認した攻撃につながり得るワークフロー例を紹介します。

※ 実際のソースコードから攻撃につながる部分を抽出し、固有名詞などは変更しています。

例1. ユーザー制御の値をそのままrun:に渡す

name: PR Check
on: pull_request
jobs:
    pr_check:
    steps:
        -name: Sonar
         run: ./gradlew sonar -Dsonar.branch=${{ github.head.ref}}

ワークフローでは、コメント本文、PRタイトル、ブランチ名などをコンテキスト変数(例:${{ github.event.comment.body }}${{ github.event.pull_request.title }}${{ github.head_ref }})として参照でき、実行時に実際のPRの値へ置き換えられます。

run:ステップはシェルコマンドとして解釈されるため、外部(攻撃者)に制御可能な値をそのまま埋め込むと、その中の記号がコマンドとして解釈される場合があります。例えばPRブランチ名に"a";lsのような文字列が入っているとワークフローは次のようなコマンドを実行してしまいます。

./gradlew sonar -Dsonar.branch="a"; ls

このような挙動はCode Injectionに該当します。対策として、中間環境変数を使う方法が推奨されます。

-name: Sonar
 run: ./gradlew sonar -Dsonar.branch="$BRANCH"
 env:
  BRANCH: ${{ github.head.ref}}

上記のようにユーザーが制御可能な変数はenv:で環境変数に格納して参照することで、シェルにコマンドとして解釈されることを防げます。

例2. pull_request_targetの誤用

name: CI
on: pull_request_target
jobs:
    lint:
    steps:
     - name: Checkout PR branch 
       uses: actions/checkout@v4
       with: 
        ref: ${{ github.event.pull_request.head.sha }}
     - name: Install Dependencies
       run: npm ci

上のワークフローは、PRのコミットをチェックアウトしてライブラリをインストールする一般的なCIの形に見えますが、攻撃につながる箇所があります。それはpull_request_targetを使っているところです。pull requestをトリガーに動くワークフローには2つのイベントがあります。

  • pull_request: コードのビルドやテストに利用します。安全性を確保するためSecretsは渡されず、リポジトリに対する権限も基本的に読み取りのみです。
  • pull_request_target: PRのラベル付けやコメント投稿など、ベースリポジトリを操作する用途に利用され、Secretsを参照可能です。2023年の仕様変更以降、デフォルトではリポジトリの読み取り権限のみが与えられますが、それ以前にリポジトリが作成されていた場合やEnterprise/Organizationの設定が変更されている場合は、デフォルトで書き込み権限も与えられます。

Secretsとは、リポジトリやOrganizationの設定に登録できる暗号化された機密情報です。(例:APIキー、トークンなど)

pull_request_targetpull_requestと比べてはるかに強い権限で動作するため、信頼できないコードを実行する可能性がある場合には使うべきではありません。上の例では、pull_request_targetの下でPRのコミットをチェックアウトし、続けてnpm ciを実行しています。npm cipackage.jsonを参照して依存関係をインストールするコマンドですが、インストール前後にスクリプトを実行できます。PR作成者はpackage.jsonを任意に変更できるため、高権限の環境で任意コード実行に至り、Secretsの窃取やリポジトリの書き換えなどのリスクが生じます。

対策としては、pull_request_targetをテストやビルドの目的で利用しないことが重要です。npm ciのように一見コードが実行されないように見える処理でも攻撃に利用される可能性があるため、高権限の環境では信頼できないコードをチェックアウトしないことを徹底します。

見つかった脆弱性について開発者の方々に共有したところ、一部はその日中に修正され、3日ですべての修正が完了しました。セキュリティを非常に重視していることがわかった体験でした。対応の際には、攻撃の仕組みや発生条件、リスクの大きさを説明するよう求められ、それに基づいて優先度やスケジュールが決まりました。攻撃の詳細とリスクの評価を正確に説明する力の重要性を実感しました。

インターンを通しての感想

チームの一員として実務に参加し、社内の広範なリポジトリや過去の脆弱性レポートに触れながら手を動かしました。社外からは当然見られず、社内でも通常は開発チームしかアクセスできないプライベートリポジトリやベータ環境について、閲覧・検証の機会を得られたのは、このインターンならではの貴重な経験でした。

一方で、セキュリティエンジニアとしての強い権限に見合う判断の重さも強く意識しました。セキュリティアセスメントはリリース前に必ず通過する工程であり、報告された脆弱性は原則として修正されなければリリースできません。自分の指摘や判断がプロダクトの安全性とリリース可否に直結することを実感し、責任の重さを認識しました。

技術面では、普段使っているLINEのサービスの内部を実際に読めたことが面白い体験でした。JavaやTypeScript、Next.jsなど多様な言語やフレームワークに触れ、ソースコードを追う力が確実に鍛えられました。同時に、言語に依存しない脆弱なポイントを見抜く視点が重要だと強く感じました。

期間中は1週間に1回、4回ほど出社し、ランチ会やともヌン(※社員同士の交流を目的としたお菓子とお茶を楽しめる社内イベント)、セキュリティ勉強会などの社内イベントを楽しみました。社食では300円台から400円台でおいしい定食が食べられ、気分転換にもなりました。チームの皆さんはセキュリティに関する知識が豊富で、新しい情報への感度も高く、多くの学びを得られました。

ソースコードを読むのが好きな方、開発が好きでセキュリティにも挑戦したい人などは非常に楽しめるインターンシップだと思います。興味がある方はぜひ応募してみてください。

余談

  • 社食の支払いはPayPayのみです。インターンに参加し出社する予定がある方は忘れずにインストールしていきましょう。
  • LINEヤフーでは月1回懇親会費が補助される制度があり、出社時にチームのランチ会に参加しました。紀尾井町タワーにあるステーキハウスに連れていっていただき、とてもおいしかったです。

メンターからの一言

斧田さんのメンターは佐藤哲平と林義徳の2名で担当しました。

今回のインターンシップでは、我々の実際の業務を一緒に進行する形で体験してもらいました。セキュリティアセスメントの業務では、CTFのような演習とは異なり、実際の大規模なコードやサービスを相手に、そこにあるかわからない脆弱性を探すことが求められます。学生にとっては決して簡単ではない課題でしたが、根気強く取り組んでいました。

慣れない実務課題にも真剣に向き合い、自分で調べながら着実に進める姿は印象的で、その過程で検証の意図や手順を整理し、わかりやすく共有してくれたことも記憶に残っています。また、学んだ内容をすぐに行動に移しながら柔軟に修正を重ねていく姿勢や、興味を持ったテーマを最後まで追い切る探究心も感じられました。

メンターの我々にとっても、一緒に議論する中で多くの学びがあり、良い時間を過ごすことができました。インターンシップは終わりましたが、またお会いできる日を楽しみにしています!