Skip to main content

Javaセキュリティのベストプラクティス10選

Cheat sheet header java

2019年9月17日

0 分で読めます

今回はチートシート形式で、オープンソースのメンテナーと開発者の双方に役立つJavaセキュリティのベストプラクティスを10個紹介します。このチートシートは、SnykのDeveloper AdvocateであるBrian Vermeerと、Java ChampionでありManicode Securityの創設者であるJim Manicoの共同制作です。

チートシートを印刷し、Javaセキュリティの10のヒントについて、以下で詳しく解説する内容もぜひお読みください。

それでは始めましょう!

Javaのセキュリティ問題

誰もが優れたコードを書くことを目指していますが、開発者が常にJavaのセキュリティを意識しているとは限りません。しかし、Javaのセキュリティ問題を防ぐことは、Javaアプリケーションのパフォーマンス、スケーラビリティ、保守性を高めることと同じくらい重要です。

また、2021年12月に公表され、脆弱なバージョンのApache Log4jを実行するアプリケーションに影響を及ぼしたLog4Shell(CVE-2021-44228)など、Javaの新たな脆弱性にも常に注意を払う必要があります。

このチートシートでは、よくあるJavaのセキュリティ問題を10個取り上げます。アプリケーションでこうした一般的なJavaのセキュリティ脆弱性を防ぐための、実践的なアドバイスと例を紹介します。

1. クエリパラメーター化でインジェクションを防ぐ

2017年版のOWASP Top 10脆弱性では、インジェクションがその年の第1位にランクインしました。JavaでよくあるSQLインジェクションでは、SQLクエリのパラメーターがクエリの固定部分に安易に連結されています。以下はJavaでSQLを安全でない方法で実行する例です。攻撃者はこれを利用して、本来許可されていない情報を取得できてしまいます。

public void selectExample(String parameter) throws SQLException {
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
   String query = "SELECT * FROM USERS WHERE lastname = " + parameter;
   Statement statement = connection.createStatement();
   ResultSet result = statement.executeQuery(query);

   printResult(result);
}

この例でパラメーターに'' OR 1=1のような値を指定すると、テーブル内のすべての項目が結果に含まれます。データベースが複数のクエリに対応している場合、パラメーターが''; UPDATE USERS SET lastname=''なら、さらに深刻な事態になりかねません。

このJavaのセキュリティリスクを防ぐには、プリペアドステートメントを使ってクエリをパラメーター化します。データベースクエリは必ずこの方法で作成してください。SQLコード全体を定義し、後からクエリにパラメーターを渡すことで、コードが読みやすくなります。何より重要なのは、SQLコードとパラメーターのデータを区別することで、悪意のある入力によるクエリの乗っ取りを防げる点です。

public void prepStatmentExample(String parameter) throws SQLException {
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
   String query = "SELECT * FROM USERS WHERE lastname = ?";
   PreparedStatement statement = connection.prepareStatement(query);
   statement.setString(1, parameter);
   System.out.println(statement);
   ResultSet result = statement.executeQuery();

   printResult(result);
}

上記の例では、入力はString型としてバインドされるため、クエリコードの一部にはなりません。この手法により、パラメーターの入力がSQLコードに干渉するのを防ぎます。

SQLインジェクション対策について詳しく知りたい方は、こちらのガイドをご覧ください。SQLインジェクション対策チートシート:攻撃を防ぐ8つのベストプラクティス

2. OpenID Connectと2FAを使用する

ID管理とアクセス制御は難しく、認証の不備がデータ侵害の原因となることも少なくありません。実際、これはOWASP Top 10脆弱性リストの第2位であり、Javaにおける重大なセキュリティリスクです。認証を独自に実装する場合は、パスワードの安全な保管、強力な暗号化、認証情報の取得など、考慮すべき点が数多くあります。多くの場合、OpenID Connectのような実績あるソリューションを使うほうが、簡単で安全です。OpenID Connect(OIDC)を使うと、複数のWebサイトやアプリでユーザーを認証できます。これにより、パスワードファイルを自分で所有・管理する必要がなくなります。OpenID Connectは、ユーザー情報を提供するOAuth 2.0の拡張仕様です。アクセストークンに加えてIDトークンを追加し、追加情報を取得できる/userinfoエンドポイントを提供します。また、エンドポイントの検出機能と動的なクライアント登録機能も備えています。

Spring Securityなどのライブラリを使えば、OpenID Connectを簡単に導入できます。システムのセキュリティをさらに強化するため、アプリケーションで2FA(二要素認証)またはMFA(多要素認証)を必ず適用してください。

Spring Bootアプリケーションにoauth2-clientとSpring Securityの依存関係を追加すると、Google、GitHub、Oktaなどのサードパーティークライアントを利用してOIDCを処理できます。アプリケーションを作成したら、アプリケーションの設定で任意のクライアントを指定して接続します。以下のように、GitHubやOktaのclient-idとclient-secretを指定できます。

pom.xml

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

application.yaml

spring:
 security:
   oauth2:
     client:
         registration:
           github:
             client-id: 796b0e5403be4729ca01
             client-secret: f379318daa27502254a05e054361074180b840a9
           okta:
             client-id: 0oa1a4wascEpYu6yk358
             client-secret: hqxj7a9lVe_TudbS2boBW7AWwxTlZiHNrJxdc_Sk
             client-name: Okta
         provider:
           okta:
             issuer-uri: https://dev-844689.okta.com/oauth2/default

3. 依存関係をスキャンして既知の脆弱性を検出する

アプリケーションが直接依存関係をいくつ使っているか、把握できていない方も多いでしょう。推移的依存関係の数も、把握できていない可能性が非常に高いです。依存関係がアプリケーション全体の大部分を占めているにもかかわらず、これはよくあることです。オープンソースの依存関係は再利用されるため、攻撃者はこれを悪用して多数の被害者を狙うことができ、攻撃対象となるケースが増えています。アプリケーションの依存関係ツリー全体に、既知のJavaセキュリティ脆弱性が含まれていないことを確認することが重要です。

Snykはアプリケーションのビルド成果物をテストし、既知の脆弱性がある依存関係を検出します。また、アプリケーションで使用しているパッケージに存在するJavaのセキュリティ脆弱性を一覧にして、ダッシュボードで確認できるようにします。

高・中・低の問題件数、依存関係の合計数、更新時刻、ソースプラットフォームが表示された、3つのリポジトリを含むSnykのプロジェクト一覧

さらに、セキュリティ問題を修正するために、アップグレード先のバージョンを提案したり、パッチを提供したりします。ソースコードリポジトリに対するプルリクエストを通じて対応できます。また、SnykはWebhookを使って、リポジトリに今後作成されるプルリクエストを自動的にテストし、新たな既知の脆弱性が持ち込まれないよう環境を保護します。

SnykはWeb UIとCLIの両方で利用できるため、CI環境に統合し、設定したしきい値を超える深刻度の脆弱性が見つかった場合にビルドを失敗させるよう設定できます。

オープンソースプロジェクト、または月間テスト数に上限のあるプライベートプロジェクトなら、Snykを無料で利用できます。

4. 機密データを慎重に扱う

顧客の個人情報やクレジットカード番号などの機密データが漏えいすると、深刻な被害につながる可能性があります。しかし、もっと目立たないケースでも、同じように有害な場合があります。たとえば、システム内の一意の識別子を使って別の呼び出しから追加データを取得できる場合、その識別子の漏えいはJavaのセキュリティ脆弱性となります。

まず、アプリケーションの設計を詳しく見直し、そのデータが本当に必要かどうかを判断してください。さらに、ログ出力、オートコンプリート、データ送信などを通じて、機密データを公開しないようにします。

機密データがログに記録されないようにする簡単な方法のひとつは、ドメインエンティティのtoString()メソッドをサニタイズすることです。これにより、機密フィールドを誤って出力するのを防げます。Lombokを使ってtoString()メソッドを生成している場合は、@ToString.Excludeを使うと、フィールドがtoString()の出力に含まれなくなります。

また、外部にデータを公開する際は十分に注意してください。たとえば、システムにユーザー名をすべて表示するエンドポイントがあっても、内部の一意な識別子まで表示する必要はありません。この識別子を使って、別のエンドポイントからユーザーに関するより機密性の高い情報を取得できる可能性があります。JacksonでPOJOをJSONにシリアライズおよびデシリアライズする場合は、@JsonIgnoreと@JsonIgnorePropertiesを使い、これらのプロパティがシリアライズまたはデシリアライズされないようにしてください。

機密データを他のサービスに送信する必要がある場合は、適切に暗号化し、たとえばHTTPSを使って接続を保護してください。

5. すべての入力をサニタイズする

クロスサイトスクリプティング(XSS)はよく知られた問題で、主にJavaScriptアプリケーションで悪用されます。しかし、Javaも例外ではありません。XSSとは、リモートで実行されるJavaScriptコードをインジェクションする攻撃です。OWASPによると、XSSを防ぐためのルール0は「許可された場所を除き、信頼できないデータを決して挿入しないこと」です。このJavaのセキュリティリスクを防ぐ基本的な方法は、信頼できないデータの使用をできるだけ避け、それ以外のデータは使用前にすべてサニタイズすることです。まずは、さまざまなエンコーダーを提供するOWASP Javaエンコーディングライブラリを試してみましょう。

<dependency>
   <groupId>org.owasp.encoder</groupId>
   <artifactId>encoder</artifactId>
   <version>1.2.2</version>
</dependency>
String untrusted = "<script> alert(1); </script>";
System.out.println(Encode.forHtml(untrusted));

// output: <script> alert(1); </script>

ユーザーが入力したテキストをサニタイズするのは当然です。では、自分たちのデータベースから取得したデータはどうでしょうか。データベースが侵害され、フィールドやドキュメントに悪意のあるテキストが埋め込まれていたらどうなるでしょう。

受信ファイルにも注意してください。多くのライブラリでZip Slip脆弱性が存在するのは、ZIPファイル内のパスがサニタイズされていないためです。../../../../foo.xyのようなパスを含むZIPファイルを展開すると、任意のファイルが上書きされる可能性があります。これはXSS攻撃ではありませんが、すべての入力をサニタイズすべき理由を示すよい例です。あらゆる入力には悪意が潜んでいる可能性があるため、適切にサニタイズする必要があります。

6. XXEを防ぐようXMLパーサーを設定する

XML外部エンティティ(XXE)が有効になっていると、以下のような悪意のあるXMLを作成し、マシン上の任意のファイルの内容を読み取ることができます。XXE攻撃がOWASP Top 10に含まれ、Javaで防ぐべきセキュリティ脆弱性であるのも当然です。多くのXMLパーサーでは外部エンティティがデフォルトで有効になっているため、JavaのXMLライブラリは特にXXEインジェクションに対して脆弱です。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE bar [
       <!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<song>
   <artist>&xxe;</artist>
   <title>Bohemian Rhapsody</title>
   <album>A Night at the Opera</album>
</song>

以下のようなDefaultHandlerとJava SAXパーサーの単純な実装では、このXMLファイルを解析してpasswdファイルの内容を表示してしまいます。ここではJava SAXパーサーを主な例として取り上げていますが、DocumentBuilderやDOM4Jなどの他のパーサーも、同様のデフォルト動作をします。

SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();

DefaultHandler handler = new DefaultHandler() {

    public void startElement(String uri, String localName,String qName,Attributes attributes) throws SAXException {
        System.out.println(qName);
    }

    public void characters(char ch[], int start, int length) throws SAXException {
        System.out.println(new String(ch, start, length));
    }
};

xerces1またはxerces2で外部エンティティとDOCTYPEを許可しないようデフォルト設定を変更すると、この種の攻撃を防止できます。

...
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();

factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxParser.getXMLReader().setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 
... 

悪意のあるXXEインジェクションの対策について、実践的な情報をお求めの場合は、OWASP XXEチートシートをご覧ください。または、外部エンティティ(XXE)インジェクション攻撃の防止方法に関する私の動画もご覧ください。

Java XML Security: How to prevent External Entity (XXE) Injection attacks

7. Javaのシリアライゼーションを避ける

Javaのシリアライゼーションを使うと、オブジェクトをバイトストリームに変換できます。このバイトストリームはディスクに保存するか、別のシステムに転送できます。反対に、バイトストリームをデシリアライズすれば、元のオブジェクトを再作成できます。

最大の問題は、デシリアライズの処理にあります。一般的には、次のようなコードになります。

ObjectInputStream in = new ObjectInputStream( inputStream );
return (Data)in.readObject();

デコードする前に、何をデシリアライズするのかを知る方法はありません。攻撃者が悪意のあるオブジェクトをシリアライズしてアプリケーションに送りつける可能性があります。readObject()を呼び出した時点で、悪意のあるオブジェクトはすでにインスタンス化されています。こうした攻撃は、脆弱なクラスがクラスパス上に必要なため不可能だと思うかもしれません。しかし、クラスパス上にあるクラスの数――自分のコード、Javaライブラリ、サードパーティーライブラリ、フレームワークを含みます――を考えると、脆弱なクラスが存在する可能性は非常に高いと言えます。

Javaのシリアライゼーションは、長年にわたり多くの問題を引き起こしてきたことから、「次々と問題を生む贈り物」とも呼ばれています。OracleはProject Amberの一環として、将来的にJavaのシリアライゼーションを廃止する予定です。しかし、実現には時間がかかる可能性があり、以前のバージョンでは修正されないでしょう。そのため、Javaのシリアライゼーションはできる限り避けるのが賢明です。ドメインエンティティにSerializableを実装する必要がある場合は、以下のように独自のreadObject()を実装するのが最善です。これにより、デシリアライズを防げます。

private final void readObject(ObjectInputStream in) throws java.io.IOException {
   throw new java.io.IOException("Deserialized not allowed");
}

入力ストリームを自分でデシリアライズする必要がある場合は、制限を設けたObjectsInputStreamを使用してください。Apache Commons IOのValidatingObjectInputStreamがよい例です。このObjectInputStreamは、デシリアライズされるオブジェクトが許可されているかどうかを確認します。

FileInputStream fileInput = new FileInputStream(fileName);
ValidatingObjectInputStream in = new ValidatingObjectInputStream(fileInput);
in.accept(Foo.class);

Foo foo_ = (Foo) in.readObject();

オブジェクトのデシリアライゼーションに関する問題は、Javaのシリアライゼーションに限りません。JSONからJavaオブジェクトへのデシリアライゼーションでも、同様の問題が発生する可能性があります。Jacksonライブラリで発生したデシリアライゼーションの問題については、ブログ記事「Jackson Deserialization Vulnerability」をご覧ください。

詳しくは、ブログ記事Javaのシリアライゼーションとデシリアライゼーション:Javaのデシリアライズ脆弱性を解説をご覧ください。

8. 強力な暗号化アルゴリズムとハッシュアルゴリズムを使用する

システムに機密データを保存する必要がある場合は、適切な暗号化を確実に実装してください。まず、対称暗号と非対称暗号など、必要な暗号化の種類を決めます。また、どの程度の強度が必要かも選択します。暗号化を強化すると、処理時間が長くなり、CPU負荷も増します。最も重要なのは、暗号化アルゴリズムを自分で実装する必要はないということです。暗号化は難しいものですが、信頼できるライブラリを使えば処理を任せられます。

たとえばクレジットカード情報を暗号化する場合、元の番号を取り出せる必要があるため、おそらく対称暗号アルゴリズムを使います。米国連邦機関で現在標準となっている対称暗号アルゴリズム、Advanced Encryption Standard(AES)を使うとしましょう。暗号化と復号のために、Javaの低レベルな暗号処理を詳しく調べる必要はありません。複雑な処理を担ってくれるライブラリの利用をおすすめします。たとえば、Google Tinkです。

<dependency>
   <groupId>com.google.crypto.tink</groupId>
   <artifactId>tink</artifactId>
   <version>1.3.0-rc1</version>
</dependency>

以下に、AESを使って認証付き暗号化(Associated Data付き暗号化、AEAD)を使用する簡単な例を示します。これにより、平文を暗号化し、暗号化はせずに認証だけを行う関連データを指定できます。

private void run() throws GeneralSecurityException {
   AeadConfig.register();
   KeysetHandle keysetHandle = KeysetHandle.generateNew(AeadKeyTemplates.AES256_GCM);

   String plaintext = "I want to break free!";
   String aad = "Queen";

   Aead aead = keysetHandle.getPrimitive(Aead.class);
   byte[] ciphertext = aead.encrypt(plaintext.getBytes(), aad.getBytes());
   String encr = Base64.getEncoder().encodeToString(ciphertext);
   System.out.println(encr);

   byte[] decrypted = aead.decrypt(Base64.getDecoder().decode(encr), aad.getBytes());
   String decr = new String(decrypted);
   System.out.println(decr);
}

パスワードについては、元のパスワードを復元する必要はなく、ハッシュ値を照合するだけなので、強力な暗号学的ハッシュアルゴリズムを使うほうが安全です。OWASP Password cheat Sheetによると、現在パスワードに最適なハッシュアルゴリズムはArgon2とBCryptです。レガシーシステムではScryptもある程度利用できます。これらはすべて、計算に多くの時間がかかる暗号学的ハッシュ(一方向関数)です。総当たり攻撃に膨大な時間がかかるため、まさにこうした性質が求められます。

Spring Securityは、幅広いアルゴリズムを優れた形でサポートしています。パスワードのハッシュ化には、Spring Security 5.0が提供するArgon2PasswordEncoderとBCryptPasswordEncoderを使ってみてください。

今日強力な暗号化アルゴリズムでも、1年後には脆弱になっている可能性があります。そのため、用途に適したアルゴリズムを使っているか、暗号化の方法を定期的に見直す必要があります。十分に検証されたセキュリティライブラリを使用し、常に最新の状態に保ちましょう。

9. Java Security Managerを有効にする

デフォルトでは、Javaプロセスに制限はありません。ファイルシステム、ネットワーク、外部プロセスなど、あらゆるリソースにアクセスできます。ただし、これらすべての権限を制御する仕組みとして、Java Security Managerがあります。デフォルトではJava Security Managerは有効になっておらず、JVMはマシンに対する無制限の権限を持ちます。JVMにシステムの特定の領域へアクセスさせたくない場合でも、実際にはアクセスできてしまいます。さらに重要なのは、Javaには予期せぬ危険な動作を引き起こすAPIが用意されていることです。

最も恐ろしいのはAttach APIだと思います。このAPIを使うと、実行中の別のJVMに接続して制御できます。たとえば、マシンにアクセスできれば、実行中のJVMのバイトコードを簡単に変更できます。Nicolas Frankelによるこちらのブログ記事では、その方法の例を紹介しています。

Java Security Managerを有効にするのは簡単です。Javaの起動時にパラメーターを追加すると、デフォルトのポリシーでSecurity Managerが有効になります。java -Djava.security.manager

ただし、デフォルトのポリシーでは、システムの目的に完全には合わない可能性があります。独自のポリシーを作成し、JVMに指定する必要があるかもしれません。java -Djava.security.manager -Djava.security.policy==/foo/bar/custom.policy

等号が2つある点に注意してください。これにより、デフォルトのポリシーが置き換えられます。等号を1つだけにすると、デフォルトのポリシーに独自のポリシーが追加されます。

JDKのアクセス権限やポリシーファイルの作成方法について詳しくは、Java公式ドキュメントをご覧ください。

注:Java 17のリリース以降、JEP 415の実装に伴い、Security Managerは「非推奨」とされています。それでも、Java 17では引き続き完全に機能します。現在、本番環境では大多数のJava開発者がJava 8またはJava 11を使用しています。そのため、将来的にSecurity Managerが削除される予定であっても、実際には今も活用する価値のある仕組みです。

10. ログとモニタリングを一元化する

セキュリティ対策は、攻撃の防止だけではありません。問題が起きたときに検知し、適切に対処できることも重要です。どのロギングライブラリを使うかは、それほど重要ではありません。大切なのは、十分な量のログを記録することです。OWASP Top 10によると、ログの不足は依然として大きな問題です。一般に、監査対象となるイベントはすべて記録する必要があります。例外、ログイン、ログイン失敗などは当然ですが、侵害された場合に何が、いつ、どのように起きたかを把握できるよう、送信元を含むすべての受信リクエストを記録することも検討してください。

ログを一元管理する仕組みを導入することをおすすめします。Log4jやlogbackを使っている場合は、たとえばElastic Stackのような一元管理システムに簡単に接続できます。Kibanaなどのツールを使えば、すべてのサーバーやシステムのログを一元的に確認し、検索して調査できます。

ログの記録に加えて、システムを継続的に監視し、各種指標を一元管理して、すぐに確認できるようにしましょう。CPU使用率の急上昇や、単一のIPアドレスからの過剰な負荷は、問題や攻撃の兆候かもしれません。ログの一元管理とリアルタイム監視をアラート機能と組み合わせ、異常が発生したときに通知を受け取れるようにしてください。

管理者パスワードのリセット、外部IPアドレスからの内部サーバーへのアクセス、‘UNION’のようなURLパラメーターは、異常を示す兆候の一例です。このような問題を適切なアラートで検知し、実際に何が起きたかを追跡できれば、大きな被害が発生する前に問題を防ぎ、脆弱性を速やかに修正できる可能性が高まります。

よくある質問

Javaセキュリティとは何ですか?

Javaセキュリティとは、悪意のあるユーザーによるアプリケーションへの侵入を防ぐために、Java開発者が講じる対策を指します。堅牢で安全なJavaコードを書くことで、アプリケーションとデータの機密性、完全性、可用性が損なわれるのを防ぎます。

Javaはセキュリティリスクですか?

Javaが必ずしもセキュリティリスクになるわけではありません。Javaを適切にアップグレードし、必要なモジュールだけを使用して、セキュリティを重視してアプリケーションを構築すれば、リスクを最小限に抑えられます。このチートシートでは、構築するアプリケーションのJavaセキュリティ脆弱性を防ぐ方法を紹介します。

Javaのセキュリティファイルはどこにありますか?

java.securityファイルは、デフォルトのセキュリティプロパティを格納するJava Runtime Environment(JRE)のファイルです。Java 8以前では、$JAVA_HOME/jre/lib/securityにあります。新しいバージョンのJavaでは、$JAVA_HOME/conf/securityにあります。

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。

続きを読む

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSecのご紹介:AI開発のスピードに対応するセキュリティ

AI駆動型開発のスピードに、従来のセキュリティワークフローは追いついていません。SnykのAgentic AppSecは、セキュリティエージェントが脆弱性を継続的に検出、修正、検証し、人が基準を定められるようにします。

blog feature ai violet
Blog

VulnBump:AIエージェントはビルドを壊さずに脆弱な依存関係を修正できるか?

VulnBumpは、実際の依存関係アップグレード307件を対象にAIエージェントをテストします。脆弱性が解消され、ビルドとテストが成功した場合にのみ、修正としてカウントされます。

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。