LoginSignup
12
14

More than 1 year has passed since last update.

Java開発者のためのセキュアコーディングのコツ10個

Last updated at Posted at 2022-01-18

本記事は2019年9月16日に公開した弊社英語ブログ10 Java security best practicesを日本語化した内容です。

logo-solid-background.png

このチートシートでは、オープンソースのメンテナーと開発者の両方に向けた、Java言語におけるセキュアコーディングのコツ10個を取り上げます。このチートシートは、SnykのデベロッパーアドボケートのBrian Vermeerと、Java チャンピオンでありManicode Securityの創設者であるJim Manicoとのコラボレーションによって作成されたものです。

このチートシートを参照し、10個のJavaセキュリティのヒントのそれぞれについて、以下の説明をよくお読みいただくことをお勧めします。

では、さっそく始めましょう。

Javaのセキュリティ問題

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

また、2021年12月に開示された、Apache log4jの脆弱なバージョンを実行しているアプリケーションに影響を与えるLog4Shell(CVE-2021-44228)など、Javaの新しい脆弱性を意識する必要があります。

このチートシートでは、Javaのセキュリティによくある10の問題について説明します。あなたが書くアプリケーションで、これらの一般的なJavaセキュリティの脆弱性を防ぐ方法について、実用的なガイダンスと例を紹介します。

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

2017年版の「OWASP Top 10 vulnerabilities」では、その年のナンバーワン脆弱性としてインジェクションがトップに登場しました。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);
}

この例のパラメータparameterが'' 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 injection cheat sheet: 8 best practices to prevent SQL injection attacks*

*訳者注記:現時点で、上記リンク先は英語です。日本語化した際には、本ブログのリンクを更新します。

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

ID管理やアクセスコントロールは難しく、認証の破たんが情報漏えいの原因になることも少なくありません。実際、これは2021年版のOWASP Top 10の1位であり、したがって、主要なJavaのセキュリティリスクです。認証を自分で実装する場合、パスワードの安全な保管、強力な暗号化、認証情報の検索など、考慮すべき点がたくさんあります。多くの場合、OpenID Connectのようなソリューションを使用する方がはるかに簡単で安全です。OpenID Connect (OIDC)は、Webサイトやアプリ間でユーザーを認証することを可能にします。これにより、パスワードファイルを所有し管理する必要がなくなります。OpenID Connectは、ユーザー情報を提供するOAuth 2.0拡張機能です。アクセストークンに加えてIDトークンを追加し、さらに情報を取得する/userinfoエンドポイントも追加されます。また、エンドポイント発見機能や動的なクライアント登録も追加されています。

Spring SecurityのようなライブラリでOpenID Connectをセットアップするのは、簡単で一般的な作業です。アプリケーションで2FA(二要素認証)または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は、アプリケーションのビルド成果物をテストし、既知の脆弱性を持つ依存関係にフラグを立てます。Snyk は、アプリケーションで使用しているパッケージに存在する Java セキュリティ脆弱性のリストをダッシュボードとして提供します。

Screenshot-2019-09-16-at-10.53.55.png

さらに、お客様のソースコードリポジトリに対するプルリクエストを通じて、セキュリティ問題を修正するためのバージョンアップの提案やパッチを提供します。また、Snykは、お客様のリポジトリで今後発生するプルリクエストが、新たな既知の脆弱性をもたらさないように、(Webhookを介して)自動的にテストされるようにすることで、お客様の環境を保護します。

SnykはWeb UIだけでなくCLIでも利用できるため、CI環境と統合し、設定した閾値を超える深刻度の脆弱性が存在する場合にビルドを中断するように設定します。

オープンソースプロジェクトや、プライベートプロジェクトでは毎月一定のテスト回数まで、Snykを無料でご利用いただけます

4. 機密データの取り扱いに注意する

顧客の個人情報やクレジットカード番号などの機密データを露出することは、たいていの場合、問題です。しかし、これよりもっと微妙なケースでも、同様に問題となる可能性があります。例えば、システム内の一意な識別子の露出は、その識別子が別の呼び出しで追加データを取得するために使用できる場合、Javaセキュリティの脆弱性になります。

まず、アプリケーションの設計をよく見て、本当にそのデータが必要なのかどうかを判断する必要があります。その上で、ロギング、オートコンプリート、データ送信などを通じて、機密データを露出しないように注意してください。

機密データをログに残さないようにする簡単な方法は、ドメインエンティティのtoString()メソッドをサニタイズすることです。この方法では、誤って機密性の高いフィールドを表示することはできません。Project 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 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インジェクションを防ぐためのより実践的な情報については、OWASP XXE Cheatsheetをご覧ください。また、「How to prevent External Entity (XXE) Injection attacks(外部エンティティ(XXE)インジェクション攻撃を防ぐ方法)」という私のビデオもご覧ください。

*訳者注記:YouTubeの自動生成ですが、上記動画は日本語字幕をつけてご覧いただけます。

7. Java のシリアライズを回避する

Javaにおけるシリアライゼーションは、オブジェクトをバイトストリームに変換することを可能にします。このバイトストリームは、ディスクに保存されるか、他のシステムに転送されます。 逆に、バイトストリームをデシリアライズすることで、元のオブジェクトを再現することができます。

最大の問題は、デシリアライズの部分にあります。一般的には次のような感じです。

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

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

Javaシリアライゼーションは、長年にわたって多くの問題を生み出してきたため、「永遠の贈り物」とも呼ばれています。Oracle社は、Project Amberの一環として、最終的にJavaのシリアライゼーションを削除する予定です。しかし、これにはしばらく時間がかかるかもしれませんし、以前のバージョンで修正される可能性も低いでしょう。したがって、Javaのシリアライゼーションはできるだけ避けた方が賢明です。ドメインエンティティにシリアライゼーションを実装する必要がある場合は、以下のように独自の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 Objectへのデシリアライゼーションにも同様の問題が含まれることがあります。ジャクソン・ライブラリのそのようなデシリアライズの問題の例は、ブログ記事Jackson Deserialization Vulnerabilityにあります。

さらに詳しく知りたい方は、弊社のブログ記事Serialization and deserialization in Java: explaining the Java deserialize vulnerabilityをご覧ください。

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でAEAD(Authenticated Encryption with Associated Data)を使用する簡単な例を示します。これにより、平文を暗号化し、認証されるべきであるが暗号化されていない関連データを提供することができます。

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によると、現在パスワードに最適なハッシュアルゴリズムは、Argon2BCryptです。レガシーシステムでは、Scryptもある程度使用できます。
3つとも暗号ハッシュ(一方向性関数)であり、計算が難しいアルゴリズムで、多くの時間を消費する。ブルートフォースアタックには時間がかかるので、これはまさにお望み通りです。

Spring securityは様々なアルゴリズムに対応する優れたサポートを提供します。Spring Security 5.0が提供するArgon2PasswordEncoderBCryptPasswordEncoderをパスワードのハッシュ化のために使ってみてください。

今日は強力な暗号化アルゴリズムでも、1年後には弱いアルゴリズムになるかもしれません。したがって、暗号化には定期的な見直しが必要で、その仕事に適したアルゴリズムを使用するようにします。これらの作業には、吟味されたセキュリティ・ライブラリを使用し、ライブラリは常に最新の状態に保つようにしてください。

9. Javaセキュリティマネージャを有効にする

デフォルトでは、Javaプロセスには何の制限もありません。ファイルシステム、ネットワーク、永遠のプロセスなど、あらゆる種類のリソースにアクセスすることができます。しかし、これらの権限をすべて制御するメカニズム、Javaセキュリティ・マネージャがあります。デフォルトでは、Javaセキュリティ・マネージャーはアクティブではなく、JVMはマシン上で無制限の権限を持っています。おそらく、JVMがシステムの特定の部分にアクセスすることは望んでいないでしょうが、JVMはアクセスすることができます。さらに重要なことは、Javaは厄介で予期せぬことをしでかすAPIを提供していることです。

一番怖いのは、Attach APIだと思います。このAPIを使うと、他の実行中のJVMに接続し、それらを制御することができます。例えば、もしあなたがそのマシンにアクセスできるなら、実行中のJVMのバイトコードを変更することはとても簡単です。Nicolas Frankelによるこのブログ記事は、これがどのように行われるかの例を示しています。

Javaセキュリティ・マネージャーを有効にするのは簡単です。追加のパラメーターでJavaを起動することで、デフォルトのポリシーであるjava -Djava.security.managerでセキュリティ・マネージャーを有効にすることができます。

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

二重の等号に注意してください - これはデフォルトのポリシーを置き換えます。1つの等号を使用すると、既定のポリシーにカスタムポリシーが追加されます。

JDKのパーミッションの詳細とポリシー・ファイルの書き方については、公式のJavaドキュメントを参照してください。

注:Java 17のリリース以降、JEP 415の実装のため、セキュリティ・マネージャーは「非推奨」とマークされています。それでも、Java 17ではまだ完全に機能しています。現在、Java開発者の大半は、Java 8またはJava 11のいずれかを実稼働で使用しています。つまり、実際には、セキュリティ・マネジャーが将来的に削除されるとしても、依然として活用できる良いメカニズムであることを意味します。

10. ロギングとモニタリングの一元化

セキュリティは予防だけではありません。何か問題が発生したときに、それに応じて行動できるように、検知する能力も必要です。どのロギング・ライブラリを使うかは、あまり重要ではありません。OWASP Top 10によると、ログの記録が不十分であることは依然として大きな問題であるため、重要なのは、多くのログを記録することです。一般的に、監査対象のイベントとなり得るものは、すべてログに記録されるべきです。例外、ログイン、ログイン失敗のようなものは明らかかもしれませんが、おそらくあなたは、すべてのリクエストをソースIPも含めて記録したいと思うでしょう。少なくとも、ハッキングされた場合に備えて、何が、いつ、どのように起こったかを知っておくことができます。

ロギングを一元化する仕組みを使うことが推奨されます。logbackのlog4jを使えば、例えばこれを集中管理されたElasticのスタックに接続するのはかなり簡単です。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にあります。

最後に

本ブログはいかがでしたか?少しでも役に立ったと思われたら、LGTMをお願いします。励みになります。

Snykでは、SNSから最新の脆弱性情報などを発信しているので、ぜひフォローと応援をよろしくお願いします。

またSnykは無料でお試しいただけます。無料トライアルはこちら。なお、無料トライアル中に生じた技術的なご質問は弊社サポートからお寄せください。UIは英語ですが、日本語で質問できます。日本人技術者が日本語で対応します。営業的な質問や相談については、こちらから弊社営業までお問い合わせください。

Snykを活用したブログは、ぜひQiitaアドカレ2021もご参照ください。

12
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
14