陽が少しづつ伸びてきましたが、寒い日が続きますね。
昔から、問題を抱えつつも幅ひろく使われているJava.
典型的なSQLインジェクションの問題など、今一度振り返り、Javaデベロッパなら知って損はない10のセキュリティプラクティスについてのこちらのブログ記事の翻訳をご紹介します
#Javaセキュリティ 10のベストプラクティス
Brian Vermeer, Jim Manico
ブライアン・フェルメール、ジム・マニコ
2019年9月16日
今回のチートシート編では、オープンソースのメンテナとデベロッパの両者に向けて、10のJavaセキュリティのベストプラクティスに焦点を当てました。本チートシートは、Snyk社のDeveloper AdvocateであるBrian Vermeer氏と、Java ChampionでManicodeSecurity社の創設者であるJim Manico氏のコラボレーションによるものです。
このチートシートを印刷して、10のJavaセキュリティのヒントのそれぞれについて、さらに詳しく読み勧めてみてください。
それでは、さっそく始めてみましょう!
##Javaのセキュリティ問題
私達デベロッパは優れたコードを書くことを目指していますが、Javaのセキュリティは、デベロッパの考えとして定着していません。しかし、Javaセキュリティの問題を防ぐことは、Javaアプリケーションのパフォーマンス、スケーラビリティ、保守性を高めることと同様に重要です。
また、2021年12月に開示された、ApacheのLog4jの脆弱なバージョンを実行するアプリケーションに影響を与える
Log4Shell(CVE-2021-44228)を確認するなど、Javaの新しい脆弱性への認識を維持する必要があります。
このチートシートでは、Javaの一般的なセキュリティに関する10の問題について説明しています。作成するアプリケーションにおいて、これらの一般的なJavaセキュリティの脆弱性を防ぐ方法ついて、実用的なガイダンスと例を紹介します。
##1.クエリのパラメータ化でインジェクションを防ぐ
2017年版のOWASPトップ10の脆弱性では、その年のナンバーワンの脆弱性として、インジェクションがトップにとなりました。Javaでの典型的な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インジェクションのチートシート:SQLインジェクション攻撃を防止するための8つのベストプラクティス
#####日本語翻訳参考記事:
序章
- クライアントサイドの入力バリデーションに依存しない
- 制限された権限を持つデータベースユーザーを使用する
- プリペアドステートメントとクエリパラメータ化を使用する
- コードをスキャンしてSQLインジェクションの脆弱性を探す
- ORMレイヤーを使用する
- ブロックリストに依存しない
- 入力バリデーションを行う
- ストアドプロシージャに注意する
##2.OpenIDConnectと2FAの併用
ID管理とアクセス制御は難しく、認証の失敗がデータ漏洩の原因となることがよくあります。実際、これは[OWASPのトップ10の脆弱性リスト](https://snyk.io/learn/owasp-top-10-vulnerabilities/)の2番目にあがっており、認証を自分で作成する際には、パスワードの安全な保管、強力な暗号化、認証情報の取得など、考慮すべき点がたくさんあります。多くの場合、[OpenID Connect](https://openid.net/connect/)のようなソリューションを使用する方がはるかに簡単で安全です。OpenID Connect(OIDC)を使用すると、Webサイトやアプリ全体でユーザーを認証することができます。所有・管理する必要がなくなります。 OpenID Connectは、ユーザー情報を提供するOAuth2.0の拡張機能です。アクセストークンに加えてIDトークンを追加します。さらに追加情報を取得するためのエンドポイント/userinfo
も追加されています。また、エンドポイント検出機能と動的クライアント登録も追加されています。
Spring Securityのようなライブラリを使ってOpenID Connectを設定するのは、簡単で一般的な作業です。アプリケーションが2FA(2要素認証)またはMFA(多要素認証)を強制するようにして、システムにさらなるセキュリティ層を追加します。
oauth2-clientとSpring securityの依存関係をSpring Bootアプリケーションに追加することで、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セキュリティ脆弱性のリストをダッシュボードに表示します。
さらに、ソースコードリポジトリへのプルリクエストを介して、バージョンのアップグレードやセキュリティ問題を修正するためのパッチの提供を提案します。Snykは、あなたのリポジトリに今後プルリクエストが上がってきた場合、そのプルリクエストが新たな既知の脆弱性をもたらさないかどうかを(Webhookを介して)自動的にテストすることで、あなたの環境を保護します。
Snykは、CLIだけでなくWeb UIからも利用できるため、CI環境と統合し、設定したしきい値を超える重大な脆弱性が存在する場合にビルドを中断すに設定します。
オープンソースプロジェクトや、毎月のテスト回数が限られているプライベートプロジェクトには、Snykを無料でお使いいただけます。
Javaアプリの脆弱性を数秒で見つけます。自動プルリクエストで修正することが可能です。
##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.XMLパーサーにXXE対策を施す
XML eXternal Entity (XXE)が有効になっていると、以下のような悪意のあるXMLを作成し、マシン上の任意のファイルの内容を読み取ることができます。XXE攻撃がOWASPトップ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("https://xml.org/sax/features/external-general-entities", false);
saxParser.getXMLReader().setFeature("https://xml.org/sax/features/external-general-entities", false);
factory.setFeature("https://apache.org/xml/features/disallow-doctype-decl", true);
...
悪意のあるXXEインジェクションを防ぐためのより実践的な情報については、OWASPXXEチートシートを参照してください。または、私のビデオ**「外部エンティティ(XXE)インジェクション攻撃を防ぐ方法」**をご覧ください.
##7.Javaのシリアル化を回避する
Javaのシリアライゼーションでは、オブジェクトをバイトストリームに変換することができます。このバイトストリームは、ディスクに保存されるか、別のシステムに転送されます。 逆に、バイトストリームをデシリアライズすると、元のオブジェクトを再現することができます。
一番の問題は、デシリアライズの部分です。典型的には次のようなものです。
ObjectInputStream in = new ObjectInputStream( inputStream );
return (Data)in.readObject();
デコードする前に、何をデシリアライズしているのかを知る方法はありません。攻撃者が悪意のあるオブジェクトをシリアル化し、アプリケーションに送信する可能性があります。readObject()
を呼び出した時点で、悪意のあるオブジェクトはすでにインスタンス化されています。このような攻撃は、クラスパス上に脆弱なクラスが必要なため、不可能だと思われるかもしれません。しかし、自分のコード、Javaライブラリ、サードパーティのライブラリやフレームワークなど、クラスパス上にあるクラスの数を考えると、脆弱なクラスが存在する可能性は非常に高いと言えます。
Javaのシリアル化は、長年にわたって多くの問題を生み出してきたことから、「与え続ける贈り物」とも呼ばれています。オラクル社は、ProjectAmberの一環として、最終的にJavaシリアライズを削除する予定です。しかし、これには時間がかかる可能性があり、以前のバージョンで修正される可能性はほとんどありません。したがって、Javaのシリアライズはできる限り避けるのが賢明です。ドメインエンティティにシリアライズを実装する必要がある場合は、以下のように、独自のreadObject()
を実装するのがベストです。これによりデシリアライズが防止されます。
private final void readObject(ObjectInputStream in) throws java.io.IOException {
throw new java.io.IOException("Deserialized not allowed");
}
入力ストリームを自分でデシリアライズする必要がある場合は、制限付きのObjectsInputStream
を使用する必要があります。この良い例が、Apache CommonsIOの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ライブラリののデシリアライズ問題の例は、ブログ投稿「JacksonDeserializationVulnerability」にあります。
さらに詳しく知りたい方は、「Javaでのシリアル化と逆シリアル化の脆弱性」をご覧ください。
##8.強力な暗号化およびハッシュアルゴリズムを使用する
システムに機密データを保存する必要がある場合は、適切な暗号化を行わなければなりません。まず最初に、対称型か非対称型かなど、必要な暗号化の種類を決める必要があります。また、どの程度の安全性を確保する必要があるかを選択する必要があります。強力な暗号化は、より多くの時間とCPUを消費します。最も重要な点は、暗号化アルゴリズムを自分で実装する必要がないことです。暗号化は難しいので、信頼できるライブラリが暗号化を解決してくれます。
例えば、クレジットカードの情報などを暗号化する場合、元の番号を取り出せるようにする必要があるため、対称型のアルゴリズムが必要になります。例えば、。現在、米国連邦組織の標準的な対称暗号化アルゴリズムであるAdvanced Encryption Standard(AES)を使用するとします。暗号化および復号化するために、低レベルのJava暗号化を深く掘り下げる理由はありません。暗号化と復号化のために、低レベルのJava暗号に深く入り込む必要はありません。難しい作業を代行してくれるライブラリを使用することをお勧めします。例えばGoogleTinkです。
<dependency>
<groupId>com.google.crypto.tink</groupId>
<artifactId>tink</artifactId>
<version>1.3.0-rc1</version>
</dependency>
以下に、AESで関連データを使用した認証付き暗号化(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
もある程度は使えます。
これら3つのアルゴリズムはいずれも暗号化ハッシュ(一方向性関数)であり、多くの時間を消費する計算困難なアルゴリズムです。ブルートフォースアタックはこの方法では何年もかかるので、これはまさにあなたが望むものです。
Spring Securityは多種多様なアルゴリズムの優れたサポートを提供しています。Spring Security5.0が提供するArgon2PasswordEncoder
とBCryptPasswordEncoder
を、パスワードハッシュの目的で使ってみてください。
今日、強力な暗号化アルゴリズムであっても、1年後には弱いアルゴリズムになっているかもしれません。したがって、暗号化を定期的に定期的に見直して、ジョブに適切なアルゴリズムを使用していることを確認する必要があります。これらのタスクには、吟味されたセキュリティ・ライブラリを使用し、ライブラリを常に最新の状態に保つようにしてください。
##9. Java SecurityManagerの有効化
デフォルトでは、Javaプロセスには何の制限もありません。ファイルシステム、ネットワーク、他のプロセスなど、あらゆる種類のリソースにアクセスできます。ただし、これらすべての権限を制御するメカニズムであるJava SecurityManagerがあります。デフォルトでは、Java Security Managerはアクティブではなく、JVMはマシンに対して無制限のパワーを持っています。JVMがシステムの特定の部分にアクセスすることはおそらく望ましくありませんが、アクセスは可能です。さらに重要なことに、Javaは厄介で予期しないことを実行できるAPIを提供します。
最も怖いのはAttachAPIだと思います。このAPIを使えば、他の稼働中のJVMに接続し、それらを制御することができます。たとえば、例えば、実行中のJVMにアクセスできれば、そのバイトのバイトコードを変更することは非常に簡単です。Nicolas Frankelによるこのブログ記事では、ここの方法の例を紹介しています。
Java Security Managerを有効にするのは簡単です。Javaを追加のパラメータで起動することで、デフォルトのポリシーであるjava -Djava.security.manager
でセキュリティマネージャを有効にします。
ただし、デフォルトのポリシーでは、システムの目的に完全には適合しない可能性があります。独自のカスタムポリシーを作成し、それをJVMに提供する必要がある場合があります。java -Djava.security.manager -Djava.security.policy==/foo/bar/custom.policy
二重の等号に注意してください - これはデフォルトのポリシーを置き換えます。単一の等号を使用すると、デフォルトのポリシーをカスタムポリシーで拡張します。
JDKの権限とポリシーファイルの記述方法の詳細については、Javaの公式ドキュメント確認してください。
注意: Java 17のリリース以降、JEP 415のが実装されたことにより、セキュリティー・マネージャーは「deprecated」とマークされています。それでもなお、Java 17では完全に機能します。とはいえ、Java 17ではまだ完全に機能しています。現在、大多数のJava開発者は、生産現場でJava 8またはJava 11を使用しています。つまり、実際には、セキュリティ・マネージャーが将来的に削除されるとしても、それでも活用すべきメカニズムであることは間違いありません。
##10.ロギングとモニタリングの一元化
セキュリティは予防だけではありません。何か問題が発生したときに、それを検知して対処できるようにすることも必要です。どのロギングライブラリを使用するかは重要ではありません。重要なのは、たくさんのログを取ることです。なぜなら、OWASPトップ10によると、不十分なログは依然として大きな問題だからです。一般的には、監査可能なイベントはすべてログに記録すべきです。例外、ログイン、失敗したログインのようなものは明らかかもしれませんが、おそらくすべての受信リクエストをその発信元も含めて記録したいでしょう。少なくとも、ハッキングされた場合に備えて、何が、いつ、どのように起こったのかを把握しておく必要があります。
ロギングを一元化する仕組みを使うことをお勧めします。たとえば、logbackのlog4jを使用している場合、これを一元化されたElastic Stackなどに接続するのはかなり簡単です。Kibanaのようなツールを使えば、すべてのサーバーやシステムのすべてのログにアクセスし、検索して調査することができます。
ロギングの次に、システムを積極的に監視し、これらの値を一元化して簡単にアクセスできるように保存する必要があります。CPUスパイクや、単一のIPアドレスからの膨大な負荷などは、問題または攻撃を示している可能性があります。一元化されたロギングとライブモニタリングをアラートと組み合わせて、何か奇妙なことが起きたときにpingを受信できるようにします。
管理者パスワードのリセット、外部IPからの内部サーバーへのアクセス、‘UNION’
のようなURLパラメータなどは、何か問題が起きていることを示すいくつかの指標に過ぎません。このような問題について適切なアラートを受け取り、実際に何が起こったのかを追跡することができれば、大きな被害を防ぐことができ、時間内にリークを修正できる可能性が高くなります。
###FAQ
####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
にあります。
######最後まで、読んでいただき、ありがとうございました!
Contents provided by:
Jesse Casman, Fumiko Doi, Content Strategists for Snyk, Japan, and Randell Degges, Community Manager for Snyk Global