4. 認証とセッション管理要件
4.1. MSTG-AUTH-1
アプリがユーザにリモートサービスへのアクセスを提供する場合、ユーザ名/パスワード認証など何らかの形態の認証がリモートエンドポイントで実行されている。
※本章での適切な認証対応については「MSTG-ARCH-2 の 1.2.1.1. 適切な認証対応」を参照する。
参考資料
owasp-mastg Verifying that Appropriate Authentication is in Place (MSTG-ARCH-2 and MSTG-AUTH-1)
owasp-mastg Testing OAuth 2.0 Flows (MSTG-AUTH-1 and MSTG-AUTH-3)
4.1.1. 認証情報の確認
資格情報の確認フローは Android 6.0 以降で利用可能であり、ユーザがロック画面保護と共にアプリ固有のパスワードを入力する必要がないようにするために使用される。代わりに、ユーザが最近デバイスにログインした場合、 confirm-credentials を使用して、 AndroidKeystore から暗号化マテリアルのロックを解除できる。つまり、ユーザが設定された時間制限 (setUserAuthenticationValidityDurationSeconds) 内にデバイスのロックを解除した場合、それ以外の場合はデバイスを再度ロック解除する必要がある。なお、 setUserAuthenticationValidityDurationSeconds は API level 30 で非推奨となっているため、現在は setUserAuthenticationParameters が推奨されている。
資格情報の確認のセキュリティは、ロック画面で設定された保護と同じくらい強力であることに注意する。これは多くの場合、単純な予測ロック画面パターンが使用されることを意味するため、認証情報の確認を使用するためにセキュリティ制御の L2 を必要とするアプリは推奨されない。
参考資料
ルールブック
4.1.1.1. 静的解析
ロック画面が設定されていることを確認する。
KeyguardManager mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
if (!mKeyguardManager.isKeyguardSecure()) {
// Show a message that the user hasn't set up a lock screen.
}
ロック画面で保護されたキーを作成する。このキーを使用するには、ユーザが最後の X 秒以内にデバイスのロックを解除するか、デバイスのロックを再度解除する必要がある。デバイスのロックを解除しているユーザとアプリを使用しているユーザが同じであることを確認するのが難しくなるため、このタイムアウトが長すぎないことを確認すること。
try { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); KeyGenerator keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); // Set the alias of the entry in Android KeyStore where the key will appear // and the constrains (purposes) in the constructor of the Builder keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setUserAuthenticationRequired(true) // Require that the user has unlocked in the last 30 seconds .setUserAuthenticationValidityDurationSeconds(30) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .build()); keyGenerator.generateKey(); } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException | KeyStoreException | CertificateException | IOException e) { throw new RuntimeException("Failed to create a symmetric key", e); }
ロック画面をセットアップして確認する。
private static final int REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS = 1; //used as a number to verify whether this is where the activity results from Intent intent = mKeyguardManager.createConfirmDeviceCredentialIntent(null, null); if (intent != null) { startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS); }
なお、 startActivityForResult は現在非推奨のため、推奨方法はルールブックを参照。
ロック画面の後にキーを使用する。
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS) { // Challenge completed, proceed with using cipher if (resultCode == RESULT_OK) { //use the key for the actual authentication flow } else { // The user canceled or didn’t complete the lock screen // operation. Go to error/cancellation flow. } } }
なお、 onActivityResult は現在非推奨のため、推奨方法はルールブックを参照。
アプリケーションフロー中にロック解除されたキーが使用されていることを確認する。たとえば、キーを使用して、ローカルストレージやリモートエンドポイントから受信したメッセージを復号できる。ユーザがキーのロックを解除したかどうかをアプリケーションが単純にチェックする場合、アプリケーションはローカル認証バイパスに対して脆弱である可能性がある。
参考資料
ルールブック
4.1.1.2. 動的解析
ユーザが正常に認証された後、キーの使用が承認される期間 (秒) を検証する。これは、 setUserAuthenticationRequired が使用されている場合にのみ必要である。
参考資料
ルールブック
4.1.2. ルールブック
4.1.2.1. 資格情報の確認フローは、セキュリティ強度について理解して使用する(推奨)
資格情報の確認のセキュリティは、ロック画面で設定された保護と同じくらい強力であることに注意する。これは多くの場合、単純な予測ロック画面パターンが使用されることを意味するため、認証情報の確認を使用するためにセキュリティ制御の L2 を必要とするアプリは推奨されない。
以下へ資格情報の確認フローのサンプルコードを示す。
private void createNewKey(String alias, int timeout) {
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(alias,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);
// Require that the user has unlocked in the last 30 seconds
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
builder.setUserAuthenticationParameters(timeout, KeyProperties.AUTH_DEVICE_CREDENTIAL);
} else {
builder.setUserAuthenticationValidityDurationSeconds(timeout);
}
keyGenerator.init(builder.build());
keyGenerator.generateKey();
} catch (NoSuchAlgorithmException | NoSuchProviderException
| InvalidAlgorithmParameterException | KeyStoreException
| CertificateException | IOException e) {
throw new RuntimeException("Failed to create a symmetric key", e);
}
}
private ActivityResultLauncher<Intent> checkCredentialLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK) {
// use the key for the actual authentication flow
} else {
// The user canceled or didn’t complete the lock screen
// operation. Go to error/cancellation flow.
}
});
private void checkCredential(Context context) {
KeyguardManager keyguardManager = (KeyguardManager)context.getSystemService(Context.KEYGUARD_SERVICE);
// Terminal authentication validity judgment
if (keyguardManager.isKeyguardSecure()) {
// Judgment of OS10 or higher
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
BiometricPrompt biometricPrompt;
// Judgment of OS11 or higher
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
biometricPrompt = new BiometricPrompt.Builder(context)
.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
.build();
} else {
biometricPrompt = new BiometricPrompt.Builder(context)
.setDeviceCredentialAllowed(true)
.build();
}
CancellationSignal cancellationSignal = new CancellationSignal();
cancellationSignal.setOnCancelListener(() -> {
// When requesting cancellation of authorization
});
Executor executors = ContextCompat.getMainExecutor(context);
BiometricPrompt.AuthenticationCallback authCallBack = new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
// Authentication Error
}
@Override
public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
// Authentication Success
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
// Authentication failure
}
};
biometricPrompt.authenticate(cancellationSignal, executors, authCallBack);
} else {
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(null, null);
checkCredentialLauncher.launch(intent);
}
}
}
これに注意しない場合、以下の可能性がある。
強度の低いセキュリティでの資格情報の確認フローを実施する可能性がある。
4.2. MSTG-AUTH-2
ステートフルなセッション管理を使用する場合、リモートエンドポイントはランダムに生成されたセッション識別子を使用し、ユーザの資格情報を送信せずにクライアント要求を認証している。
4.2.1. セッション情報の管理
ステートフル(または「セッションベース」)認証は、クライアントとサーバの両方に認証レコードがあることが特徴である。認証の流れは以下の通りである。
アプリはユーザの認証情報を含む要求をバックエンドサーバに送信する。
サーバは認証情報を確認する。認証情報が有効な場合、サーバはランダムなセッション ID とともに新しいセッションを作成する。
サーバはセッション ID を含む応答をクライアントに送信する。
クライアントは以降のすべての要求でセッション ID を送信する。サーバはセッション ID を検証し、関連するセッションレコードを取得する。
ユーザがログアウトした後、サーバ側のセッションレコードは破棄され、クライアントはセッション ID を破棄する。
セッションの管理が不適切な場合、様々な攻撃に対して脆弱となり、正規のユーザのセッションが侵害され、攻撃者がそのユーザになりすますことが可能になる。その結果、データの損失、機密性の低下、不正な操作が生じる可能性がある。
参考資料
4.2.1.1. セッション管理のベストプラクティス
機密情報や機能を提供するサーバ側のエンドポイントを探し出し、一貫した認可の実施を検証する。バックエンドサービスは、ユーザのセッション ID またはトークンを検証し、ユーザがリソースにアクセスするための十分な権限を持っていることを確認する必要がある。セッション ID またはトークンがない、または無効な場合、要求を拒否する必要がある。
確認事項
セッション ID は、サーバ側でランダムに生成される。
ID は簡単に推測できないようにする。(適切な長さとエントロピーを使用する)
セッション ID は常に安全な接続(例 : HTTPS )で交換される。
モバイルアプリはセッション ID を永続的なストレージに保存しない。
サーバは、ユーザが権限を要するアプリケーションにアクセスするたびに、セッションを検証する。(セッション ID は有効で、適切な認証レベルに対応している必要がある)
セッションはサーバ側で終了し、タイムアウトまたはユーザがログアウトした後にモバイルアプリ内でセッション情報が削除される。
認証は実装するべきでなく、実績のあるフレームワークの上に構築する必要がある。多くの一般的なフレームワークは、既製の認証およびセッション管理機能を提供している。アプリが認証のためにフレームワークのAPIを使用する場合、ベストプラクティスのためにフレームワークのセキュリティドキュメントを確認する。一般的なフレームワークのセキュリティガイドは、以下のリンクで入手できる。
サーバ側の認証を検証するための最適なリソースは、OWASP Web Testing Guide 、特に、「 Testing Authentication 」と「 Testing Session Management 」の章に掲載されている。
参考資料
ルールブック
4.2.2. ルールブック
4.2.2.1. モバイルアプリはセッション ID を永続的なストレージに保存しないことを確認する(必須)
永続的なストレージにセッション ID を保存すると、ユーザにより読み書きされたり、第三者により利用されたりする恐れがある。そのため、セッション ID はこのようなストレージには保存しないようにする必要がある。
※非推奨なルールのため、サンプルコードはなし。
これに違反する場合、以下の可能性がある。
セッション ID をユーザにより読み書きされたり、第三者により利用されたりする恐れがある。
4.3. MSTG-AUTH-3
ステートレスなトークンベースの認証を使用する場合、サーバはセキュアなアルゴリズムを使用して署名されたトークンを提供している。
4.3.1. トークンの管理
トークンベースの認証は、 HTTP 要求ごとに署名されたトークン(サーバによって検証される)を送信することによって実装される。最も一般的に使用されているトークン形式は、 RFC7519 で定義されている JSON Web Token である。 JWT は、完全なセッション状態を JSON オブジェクトとしてエンコードすることができる。そのため、サーバはセッションデータや認証情報を保存する必要がない。
JWT トークンは、ドットで区切られた 3 つの Base64Url エンコード部分から構成されている。トークンの構造は以下の通りである。
base64UrlEncode(header).base64UrlEncode(payload).base64UrlEncode(signature)
次の例は、 Base64Url でエンコードされた JSON Web Token を示している。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva
G4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
ヘッダーは通常 2 つの部分から構成される。トークンタイプ( JWT )と、署名を計算するために使用されるハッシュアルゴリズムである。上の例では、ヘッダーは以下のようにデコードされる。
{"alg":"HS256","typ":"JWT"}
トークンの 2 番目の部分はペイロードで、いわゆるクレームを含んでいる。クレームは、あるエンティティ(通常はユーザ)に関するステートメントと、追加のメタデータである。 例を以下に示す。
{"sub":"1234567890","name":"John Doe","admin":true}
署名は、符号化されたヘッダー、符号化されたペイロード、および秘密値に対して、 JWT ヘッダーで指定されたアルゴリズムを適用することによって作成される。例えば、 HMAC SHA256 アルゴリズムを使用する場合、署名は以下のように作成される。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
この秘密情報は、認証サーバとバックエンドサービスの間で共有され、クライアントはそれを認識しないことに注意する。これは、トークンが正当な認証サービスから取得されたことを証明するものである。また、クライアントがトークンに含まれるクレームを改ざんすることも防ぐことができる。
参考資料
4.3.2. 静的解析
サーバとクライアントが使用している JWT ライブラリを特定する。使用している JWT ライブラリに既知の脆弱性があるかどうかを確認する。
実装が JWT の ベストプラクティス を遵守していることを確認する。
トークンを含むすべての受信要求について、 HMAC が検証されることを確認する。
秘密署名鍵または HMAC 秘密鍵の場所を確認する。キーはサーバに保管し、決してクライアントと共有してはならない。発行者と検証者のみが利用可能であるべきである。
JWT に個人を特定できる情報のような機密データが埋め込まれていないことを確認する。何らかの理由でトークン内にそのような情報を送信する必要があるアーキテクチャの場合、ペイロードの暗号化が適用されていることを確認する。 OWASP JWT Cheat Sheet にある Java 実装のサンプルを参照する。
JWT に一意な識別子を与える jti ( JWT ID ) クレームでリプレイ攻撃に対処していることを確認する。
クロスサービスリレー攻撃には、トークンがどのアプリケーションのためのものかを定義する aud ( audience ) クレームで対処していることを確認する。
トークンは、 KeyStore (Android) により、モバイルデバイス上に安全に保管されていることを確認する。
参考資料
ルールブック
4.3.2.1. ハッシュアルゴリズムの強制
攻撃者はトークンを変更し、「 none 」キーワードを使って署名アルゴリズムを変更し、トークンの完全性がすでに検証されていることを示すことによってこれを実行する。ライブラリによっては、「 none 」アルゴリズムで署名されたトークンを、検証済みの署名を持つ有効なトークンであるかのように扱い、アプリケーションは変更されたトークンの要求を信頼する可能性がある。
例えば、 Java アプリケーションでは、検証コンテキストを作成する際に、期待するアルゴリズムを明示的に要求する必要がある。
// HMAC key - Block serialization and storage as String in JVM memory
private transient byte[] keyHMAC = ...;
//Create a verification context for the token requesting explicitly the use of the HMAC-256 HMAC generation
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)).build();
//Verify the token; if the verification fails then an exception is thrown
DecodedJWT decodedToken = verifier.verify(token);
参考資料
ルールブック
4.3.2.2. トークンの有効期限
署名されたステートレス認証トークンは、署名キーが変更されない限り永久に有効である。トークンの有効期限を制限する一般的な方法は、有効期限を設定することである。トークンに "exp" expiration claimが含まれ、バックエンドが期限切れのトークンを処理しないことを確認する。
トークンを付与する一般的な方法は、アクセストークンとリフレッシュトークンを組み合わせることである。ユーザがログインすると、バックエンドサービスは有効期間の短いアクセストークンと有効期間の長いリフレッシュトークンを発行する。アクセストークンの有効期限が切れた場合、アプリケーションはリフレッシュトークンを使って新しいアクセストークンを取得することができる。
機密データを扱うアプリの場合、リフレッシュトークンの有効期限が適切な期間であることを確認する必要がある。次のコード例では、リフレッシュトークンの発行日をチェックするリフレッシュトークン API を紹介する。トークンが 14 日以上経過していない場合、新しいアクセストークンが発行される。それ以外の場合は、アクセスが拒否され、再度ログインするよう促される。
app.post('/renew_access_token', function (req, res) {
// verify the existing refresh token
var profile = jwt.verify(req.body.token, secret);
// if refresh token is more than 14 days old, force login
if (profile.original_iat - new Date() > 14) { // iat == issued at
return res.send(401); // re-login
}
// check if the user still exists or if authorization hasn't been revoked
if (!valid) return res.send(401); // re-logging
// issue a new access token
var renewed_access_token = jwt.sign(profile, secret, { expiresInMinutes: 60*5 });
res.json({ token: renewed_access_token });
});
参考資料
ルールブック
4.3.3. 動的解析
動的解析を行いながら、以下の JWT の脆弱性を調査する。
クライアント上のトークン保存場所
JWT を使用するモバイルアプリの場合、トークンの保管場所を確認する必要がある。
署名キーのクラック
トークン署名は、サーバ上の秘密キーを介して作成される。 JWT を取得した後、オフラインで秘密鍵をブルートフォースするためのツールを選択する。
情報の開示
Base64Url でエンコードされた JWT をデコードし、それがどのようなデータを送信しているか、そのデータが暗号化されているかどうかを調べることができる。
ハッシュアルゴリズムの改ざん
非対称型アルゴリズムの使用。 JWT は RSA や ECDSA のような非対称アルゴリズムをいくつか提供している。これらのアルゴリズムが使用される場合、トークンは秘密鍵で署名され、検証には公開鍵が使用される。もしサーバが非対称アルゴリズムで署名されたトークンを期待し、 HMAC で署名されたトークンを受け取った場合、サーバは公開鍵を HMAC 秘密鍵として扱う。そして、公開鍵はトークンに署名するために HMAC 秘密鍵として採用され、悪用される可能性がある。
トークンヘッダの alg 属性を修正し、 HS256 を削除して none に設定し、空の署名 ( 例えば、 signature = "" ) を使用する。このトークンを使用し、要求で再生する。いくつかのライブラリは、 none アルゴリズムで署名されたトークンを、 検証済みの署名を持つ有効なトークンとして扱う。これにより、攻撃者は自分自身の「署名付き」トークンを作成することができる。
上記のような脆弱性をテストするために、 2 種類の Burp プラグインが用意されている。
また、その他の情報については、 OWASP JWT Cheat Sheet を必ず確認すること。
参考資料
4.3.4. OAuth 2.0 のフローテスト
OAuth 2.0 は、 API やウェブ対応アプリケーションのネットワーク上で認可の決定を伝えるための委任プロトコルを定義している。ユーザ認証アプリケーションなど、さまざまなアプリケーションで使用されている。
OAuth2 の一般的な用途は以下の通りである。
ユーザのアカウントを使用してオンラインサービスにアクセスする許可をユーザから取得する。
ユーザに代わってオンラインサービスに対する認証を行う。
認証エラーの処理。
OAuth 2.0 によると、ユーザのリソースにアクセスしようとするモバイルクライアントは、まずユーザに認証サーバに対する認証を要求する必要がある。ユーザの承認後、認証サーバはユーザに代わってアプリが動作することを許可するトークンを発行する。 OAuth2 仕様では、特定の種類の認証やアクセストークンの形式は定義されていないことに注意する。
OAuth 2.0 では、 4 つの役割が定義されている。
リソースオーナー:アカウントの所有者
クライアント:アクセストークンを使ってユーザのアカウントにアクセスしようとするアプリケーション
リソースサーバ:ユーザアカウントをホストする
認可サーバ:ユーザの身元を確認し、アプリケーションにアクセストークンを発行する
注:API はリソースオーナーと認可サーバの両方の役割を果たす。したがって、ここでは両方を API と呼ぶことにする。
図中の各ステップについて、以下でより詳細に説明する。
アプリケーションは、サービス・リソースにアクセスするためのユーザ認証を要求する。
ユーザが要求を承認すると、アプリケーションは認証付与を受け取る。認可付与は、いくつかの形式(明示的、暗黙的など)を取ることができる。
アプリケーションは、認可サーバ(API)にアクセストークンを要求する際に、認証付与と一緒に自身の ID の認証を提示する。
アプリケーションの ID が認証され、認証付与が有効である場合、認可サーバ( API )はアプリケーションにアクセストークンを発行し、認可プロセスを完了する。アクセストークンには、リフレッシュトークンが付随している場合がある。
アプリケーションはリソースサーバ( API )にリソースを要求し、認証のためにアクセストークンを提示する。アクセストークンは、いくつかの方法で使用することができる(ベアラートークンなど)。
アクセストークンが有効であれば、リソースサーバ( API )はアプリケーションにリソースを提供する。
参考資料
4.3.5. OAuth 2.0 のベストプラクティス
以下のベストプラクティスが守られていることを確認する。
ユーザエージェント
ユーザは信頼を視覚的に確認する方法(例: TLS(Transport Layer Security) 確認、ウェブサイトの仕組み)を持つべきである。
中間者攻撃を防ぐために、クライアントはサーバの完全修飾ドメイン名を、接続確立時にサーバが提示した公開鍵で検証する必要がある。
付与の種類
ネイティブアプリでは、暗黙的な付与ではなく、コード付与を使用する必要がある。
コード付与を使用する場合、コード付与を保護するために PKCE ( Proof Key for Code Exchange ) を実装する必要があります。サーバ側でも実装されていることを確認する。
認可「コード」は有効期限が短く、受信後すぐに使用されるべきである。認可コードが一時的なメモリ上にのみ存在し、保存やログに記録されないことを確認する。
クライアントシークレット
クライアントはなりすまされる可能性があるため、クライアントの ID を証明するために共有シークレットを使うべきではない(「 client_id 」がすでに証明の役目を果たしている)。もし、クライアントシークレットを使用する場合は、安全なローカルストレージに保存されていることを確認する。
エンドユーザの認証情報
TLS などのトランスポート層方式でエンドユーザ認証情報の送信を保護する。
トークン
アクセストークンは一時的なメモリに保存する。
アクセストークンは、暗号化された接続で送信する必要がある。
エンドツーエンドの機密性が保証されない場合や、トークンが機密情報や取引へのアクセスを提供する場合は、アクセストークンの範囲と期間を縮小する。
アプリケーションがアクセストークンをベアラートークンとして使用し、クライアントを識別する他の方法がない場合、トークンを盗んだ攻撃者はそのスコープとそれに関連するすべてのリソースにアクセスできることに留意する。
リフレッシュトークンは、安全なローカルストレージに保管する。
参考資料
ルールブック
4.3.5.1. 外部ユーザエージェントと組み込みユーザエージェント
OAuth2 認証は、外部のユーザエージェント( Chrome や Safari など)を介して行うことも、アプリ自体(アプリに埋め込まれた WebView や認証ライブラリなど)を介して行うこともできる。どちらのモードが本質的に「優れている」ということはなく、どのモードを選択するかはコンテキストに依存する。
外部ユーザエージェントを使用する方法は、ソーシャルメディアアカウント( Facebook 、 Twitter など)と対話する必要があるアプリケーションで選択される方法である。この方法のメリットは以下の通りである。
ユーザの認証情報は、決してアプリに直接公開されることはない。このため、ログイン時にアプリが認証情報を取得することはできない(「認証情報のフィッシング」)。
アプリ自体に認証ロジックをほとんど追加する必要がないため、コーディングミスを防ぐことができる。
マイナス面としては、ブラウザの動作を制御する方法(証明書のピン留めを有効にする等)がないことである。
閉じたエコシステムの中で動作するアプリの場合、埋め込み認証の方が良い選択となる。例えば、 OAuth2 を使って銀行の認証サーバからアクセストークンを取得し、そのアクセストークンを使って多くのマイクロサービスにアクセスする銀行アプリを考える。この場合、クレデンシャルフィッシングは実行可能なシナリオではない。認証プロセスは、外部のコンポーネントを信頼するのではなく、(できれば)慎重に保護されたバンキングアプリの中で行うことが望ましい。
参考資料
ルールブック
4.3.6. その他の OAuth 2.0 のベストプラクティス
その他のベストプラクティスや詳細情報については、以下のソースドキュメントを参照する。
RFC6749 - The OAuth 2.0 Authorization Framework (October 2012)
RFC6819 - OAuth 2.0 Threat Model and Security Considerations (January 2013)
参考資料
4.3.7. ルールブック
4.3.7.1. 使用している JWT ライブラリの特定及び既知の脆弱性があるかどうかを確認する(必須)
以下の内容を確認して、脆弱性があるか確認する必要がある。
使用している JWT ライブラリを特定し、既知の脆弱性があるかどうかを確認する。
トークンを含むすべての受信要求について、 HMAC が検証されることを確認する。
秘密署名鍵または HMAC 秘密鍵の場所を確認する。キーはサーバに保管し、決してクライアントと共有してはならない。発行者と検証者のみが利用可能であるべきである。
JWT に個人を特定できる情報のような機密データが埋め込まれていないことを確認する。何らかの理由でトークン内にそのような情報を送信する必要があるアーキテクチャの場合、ペイロードの暗号化が適用されていることを確認する。 OWASP JWT Cheat Sheet にある Java 実装のサンプルを参照する。
JWT に一意な識別子を与える jti ( JWT ID ) クレームでリプレイ攻撃に対処していることを確認する。
クロスサービスリレー攻撃には、トークンがどのアプリケーションのためのものかを定義する aud ( audience ) クレームで対処していることを確認する。
トークンは、 KeyStore (Android) により、モバイルデバイス上に安全に保管されていることを確認する。
以下サンプルコードは、 KeyStore の暗号化キーにより暗号化したトークンを SharedPreferences へ保存、及び SharedPreferences から取り出して復号処理の一例。
val PROVIDER = "AndroidKeyStore"
val ALGORITHM = "RSA"
val CIPHER_TRANSFORMATION = "RSA/ECB/PKCS1Padding"
/**
* トークンを暗号化して SharedPreferences へ保存する
*/
fun saveToken(context: Context, token: String) {
val encryptedToken = encrypt(context, "token", token)
val editor = getSharedPreferences(context).edit()
editor.putString("encrypted_token", encryptedToken).commit()
}
/**
* トークンを復号して SharedPreferences から取得する
*/
fun getToken(context: Context): String? {
val encryptedToken = getSharedPreferences(context).getString("encrypted_token", null)
if (encryptedToken == null) {
return encryptedToken
}
val token = decrypt("token", encryptedToken)
return token
}
/**
* テキストを暗号化する
* @param context
* @param alias 対称鍵を識別するためのエリアス。用途ごとに一意にする。
* @param plainText 暗号化したいテキスト
* @return 暗号化されBase64でラップされた文字列
*/
fun encrypt(context: Context, alias: String, plainText: String): String {
val keyStore = KeyStore.getInstance(PROVIDER)
keyStore.load(null)
// 対称鍵がない場合生成
if (!keyStore.containsAlias(alias)) {
val keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM, PROVIDER)
keyPairGenerator.initialize(createKeyPairGeneratorSpec(context, alias))
keyPairGenerator.generateKeyPair()
}
val publicKey = keyStore.getCertificate(alias).getPublicKey()
val privateKey = keyStore.getKey(alias, null)
// 公開鍵で暗号化
val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
val bytes = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
// SharedPreferencesに保存しやすいようにBase64でString化
return Base64.encodeToString(bytes, Base64.DEFAULT)
}
/**
* 暗号化されたテキストを復化する
* @param alias 対称鍵を識別するためのエリアス。用途ごとに一意にする。
* @param encryptedText encryptで暗号化されたテキスト
* @return 復号された文字列
*/
fun decrypt(alias: String, encryptedText: String): String? {
val keyStore = KeyStore.getInstance(PROVIDER)
keyStore.load(null)
if (!keyStore.containsAlias(alias)) {
return null
}
// 秘密鍵で復号
val privateKey = keyStore.getKey(alias, null)
val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, privateKey)
val bytes = Base64.decode(encryptedText, Base64.DEFAULT)
val b = cipher.doFinal(bytes)
return String(b)
}
/**
* 対称鍵を生成する
*/
fun createKeyPairGeneratorSpec(context: Context, alias: String): KeyPairGeneratorSpec {
val start = Calendar.getInstance()
val end = Calendar.getInstance()
end.add(Calendar.YEAR, 100)
return KeyPairGeneratorSpec.Builder(context)
.setAlias(alias)
.setSubject(X500Principal(String.format("CN=%s", alias)))
.setSerialNumber(BigInteger.valueOf(1000000))
.setStartDate(start.getTime())
.setEndDate(end.getTime())
.build()
}
これに違反する場合、以下の可能性がある。
使用する JWT ライブラリに関する脆弱性を悪用される。
4.3.7.2. トークンは、 KeyStore (Android) により、モバイルデバイス上に安全に保管されていることを確認する(必須)
トークンは KeyStore によりモバイルデバイス上に安全に保管する必要がある。これを怠った場合、第三者によるトークンを利用したサーバ認証が可能になる恐れがある。
KeyStore の使用方法は以下ルールを参照する。
データストレージとプライバシー要件ルールブック
これに違反する場合、以下の可能性がある。
第三者によるトークンを利用したサーバ認証が可能になる。
4.3.7.3. 期待するハッシュアルゴリズムを要求する必要がある(必須)
攻撃者はトークンを変更し、「 none 」キーワードを使って署名アルゴリズムを変更し、トークンの完全性がすでに検証されていることを示す場合がある。 よって、 Java アプリケーションでは検証コンテキストを作成する際に、期待するアルゴリズムを明示的に要求する必要がある。
// HMAC key - Block serialization and storage as String in JVM memory
private transient byte[] keyHMAC = ...;
//Create a verification context for the token requesting explicitly the use of the HMAC-256 HMAC generation
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)).build();
//Verify the token; if the verification fails then an exception is thrown
DecodedJWT decodedToken = verifier.verify(token);
これに違反する場合、以下の可能性がある。
「 none 」アルゴリズムで署名されたトークンを、検証済みの署名を持つ有効なトークンであるかのように扱い、アプリケーションは変更されたトークンの要求を信頼する可能性がある。
4.3.7.4. リフレッシュトークンの有効期限に適切な期間を設定する(必須)
リフレッシュトークンの有効期限が適切な期間であることを確認する必要がある。リフレッシュトークンの有効期限内の場合は、新しいアクセストークンの発行をし、有効期限外の場合は、アクセスを拒否してユーザに再ログインを促す必要性がある。
これに違反する場合、以下の可能性がある。
有効期限が不適切な場合はログインユーザを第三者が利用できる可能性がある。
4.3.7.5. OAuth 2.0 のベストプラクティスの遵守(必須)
以下の OAuth 2.0 のベストプラクティスを遵守すること。
ユーザエージェント
ユーザは信頼を視覚的に確認する方法(例: TLS(Transport Layer Security) 確認、ウェブサイトの仕組み)を持つべきである。
中間者攻撃を防ぐために、クライアントはサーバの完全修飾ドメイン名を、接続確立時にサーバが提示した公開鍵で検証する必要がある。
付与の種類
ネイティブアプリでは、暗黙的な付与ではなく、コード付与を使用する必要がある。
コード付与を使用する場合、コード付与を保護するために PKCE ( Proof Key for Code Exchange ) を実装する必要があります。サーバ側でも実装されていることを確認する。
認可「コード」は有効期限が短く、受信後すぐに使用されるべきである。認可コードが一時的なメモリ上にのみ存在し、保存やログに記録されないことを確認する。
クライアントシークレット
クライアントはなりすまされる可能性があるため、クライアントの ID を証明するために共有シークレットを使うべきではない(「 client_id 」がすでに証明の役目を果たしている)。もし、クライアントシークレットを使用する場合は、安全なローカルストレージに保存されていることを確認する。
エンドユーザの認証情報
TLS などのトランスポート層方式でエンドユーザ認証情報の送信を保護する。
トークン
アクセストークンは一時的なメモリに保存する。
アクセストークンは、暗号化された接続で送信する必要がある。
エンドツーエンドの機密性が保証されない場合や、トークンが機密情報や取引へのアクセスを提供する場合は、アクセストークンの範囲と期間を縮小する。
アプリケーションがアクセストークンをベアラートークンとして使用し、クライアントを識別する他の方法がない場合、トークンを盗んだ攻撃者はそのスコープとそれに関連するすべてのリソースにアクセスできることに留意する。
リフレッシュトークンは、安全なローカルストレージに保管する。
これに違反する場合、以下の可能性がある。
脆弱な OAuth 2.0 の実装となる可能性がある。
4.3.7.6. OAuth2 認証で使用される主な推奨ライブラリ(推奨)
OAuth2 認証の実装を独自開発しないことが重要となる。
OAuth2 認証を実現するために利用されるライブラリは主に以下である。
Google Play Services (Google)
以下サンプルコードは、 Google Play Services での OAuth2 認証の一例。
/**
* Google SignIn
*/
private fun requestOAuth() {
val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).build()
activityResultLauncher.launch(GoogleSignIn.getClient(requireContext(),signInOptions).signInIntent)
}
これに注意しない場合、以下の可能性がある。
脆弱な OAuth 2.0 の実装となる可能性がある。
4.3.7.7. OAuth2 認証で外部ユーザエージェントを使用する場合はメリット、デメリットを理解して使用する(推奨)
OAuth2 認証は、外部のユーザエージェント( Chrome や Safari など)を介して行うことできる。 外部ユーザエージェントを利用時のメリット、デメリットは以下である。
メリット
ユーザの認証情報は、決してアプリに直接公開されることはない。このため、ログイン時にアプリが認証情報を取得することはできない(「認証情報のフィッシング」)。
アプリ自体に認証ロジックをほとんど追加する必要がないため、コーディングミスを防ぐことができる。
デメリット
ブラウザの動作を制御する方法(証明書のピン留めを有効にする等)がない。
これに注意しない場合、以下の可能性がある。
証明書のピン留めを有効にする必要がある際に、利用することができない。
4.4. MSTG-AUTH-4
ユーザがログアウトする際に、リモートエンドポイントは既存のセッションを終了している。
4.4.1. リモートセッション情報の破棄
このテストケースの目的は、ログアウト機能を検証し、それがクライアントとサーバの両方で効果的にセッションを終了させ、ステートレストークンを無効にするかどうかを判断することである。
サーバ側セッションの破棄に失敗することは、ログアウト機能の実装で最もよくあるエラーの一つである。このエラーは、ユーザがアプリケーションからログアウトした後も、セッションやトークンを保持し続ける。有効な認証情報を取得した攻撃者は、その情報を使い続け、ユーザのアカウントを乗っ取ることが可能である。
多くのモバイルアプリは、ユーザを自動的にログアウトさせない。顧客にとって不便だから、あるいはステートレス認証の実装時に決定されたからなど、さまざまな理由が考えられる。しかし、アプリケーションにはログアウト機能が必要であり、ベストプラクティスに従って実装し、ローカルに保存されたトークンやセッション識別子をすべて破棄する必要がある。セッション情報がサーバに保存されている場合、そのサーバにログアウト要求を送信することによっても破棄されなければならない。リスクの高いアプリケーションの場合、トークンを無効にする必要がある。トークンやセッション識別子を破棄しないと、トークンが流出した場合にアプリケーションに不正にアクセスされる可能性がある。なお、適切に消去されない情報は、デバイスのバックアップ時など、後で流出する可能性があるため、他の機密情報も同様に破棄する必要がある。
4.4.2. 静的解析
サーバコードが利用可能な場合は、ログアウト機能によってセッションが正しく終了することを確認する。この検証は、テクノロジーによって異なる。以下は、サーバ側のログアウトが正しく行われるためのセッション終了の様々な例である。
ステートレス認証でアクセストークンやリフレッシュトークンを使用する場合は、モバイルデバイスから破棄する必要がある。リフレッシュトークンはサーバ上で無効化する必要がある。
4.4.3. 動的解析
動的アプリケーション解析用のインターセプションプロキシを使用し、以下の手順を実行して、ログアウトが正しく実装されているかどうかを確認する。
アプリケーションにログインする。
認証を必要とするリソースにアクセスする。通常は、アカウントに属する個人情報の要求である。
アプリケーションからログアウトする。
手順 2 の要求を再送信して、再度データにアクセスする。
サーバ上でログアウトが正しく実装されている場合は、エラーメッセージまたはログインページへのリダイレクトがクライアントに返される。一方、手順 2 で得たのと同じレスポンスを受け取る場合、トークンあるいはセッション ID はまだ有効で、サーバ上で正しく終了していない。OWASP ウェブテストガイド ( WSTG-SESS-06 には、詳しい説明とより多くのテストケースが含まれている。
参考資料
ルールブック
4.4.4. ルールブック
4.4.4.1. アプリにログイン機能が存在する場合のリモートセッション情報の破棄のベストプラクティス(推奨)
アプリにログイン機能が存在する場合、以下のベストプラクティスに従いリモートセッション情報を破棄する。
アプリケーションにログアウト機能を用意する。
ログアウト時にローカルに保存されたトークンやセッション識別子をすべて破棄する。
セッション情報がサーバに保存されている場合、サーバにログアウト要求を送信して破棄する。
リスクの高いアプリの場合、トークンを無効にする。
トークンやセッション識別子を破棄しないと、トークンが流出した場合にアプリケーションに不正にアクセスされる可能性がある。 破棄されない情報は、デバイスのバックアップ時など、後で流出する可能性があるため、他の機密情報も同様に破棄する必要がある。
これに注意しない場合、以下の可能性がある。
ログイン情報がメモリ内に残り、第三者に利用される。
4.5. MSTG-AUTH-5
パスワードポリシーが存在し、リモートエンドポイントで実施されている。
4.5.1. パスワードポリシー遵守
パスワードの強度は、認証にパスワードが使用される場合の重要な懸念事項である。パスワードポリシーは、エンドユーザが遵守すべき要件を定義するものである。パスワードポリシーは通常、パスワードの長さ、パスワードの複雑さ、およびパスワードのトポロジーを指定する。「強力な」パスワードポリシーは、手動または自動によるパスワードクラッキングを困難または不可能にする。以下のセクションでは、パスワードのベストプラクティスに関するさまざまな領域をカバーする。より詳細な情報は、 OWASP Authentication Cheat Sheet を参照する。
参考資料
4.5.2. 静的解析
パスワードポリシーの存在を確認し、長さと無制限の文字セットに焦点を当てた OWASP Authentication Cheat Sheet に従って、実装されたパスワードの複雑さの要件を検証すること。ソースコードに含まれるパスワード関連の関数をすべて特定し、それぞれで検証チェックが行われていることを確認する。パスワード検証機能を確認し、パスワードポリシーに違反するパスワードが拒否されることを確認する。
参考資料
ルールブック
4.5.2.1. zxcvbn
zxcvbn は、パスワードクラッカーにヒントを得て、パスワードの強度を推定するために使用できる一般的なライブラリである。 JavaScript で利用可能であるが、サーバ側では他の多くのプログラミング言語でも利用可能である。インストール方法は様々なので、 Github のリポジトリを確認する。インストールすると、 zxcvbn を使って、パスワードの複雑さと解読に必要な推測回数を計算することができる。
HTML ページに zxcvbn JavaScript ライブラリを追加した後、ブラウザのコンソールで zxcvbn コマンドを実行すると、スコアを含むパスワードのクラックの可能性に関する詳細情報を取得できる。
スコアは以下のように定義され、例えばパスワードの強度バーに使用することができる。
0 # too guessable: risky password. (guesses < 10^3)
1 # very guessable: protection from throttled online attacks. (guesses < 10^6)
2 # somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)
3 # safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)
4 # very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)
なお、 zxcvbn はアプリ開発者が Java(または他の)実装を使って実装することもでき、ユーザに強力なパスワードを作成するように導くことができる。
参考資料
4.5.3. Have I Been Pwned : PwnedPasswords
一要素認証スキーム(パスワードのみなど)に対する辞書攻撃の成功の可能性をさらに低くするために、パスワードがデータ漏洩で流出したかどうかを検証することができる。これは、 Troy Hunt 氏による Pwned Passwords API をベースにしたサービス( api.pwnedpasswords.com で入手可能)を使って行うことができる。例えば、コンパニオンウェブサイトの「 Have I been pwned ? 」である。この API は、パスワード候補の SHA-1 ハッシュをもとに、指定されたパスワードのハッシュが、このサービスが収集したさまざまなデータ漏洩の事例の中で見つかった回数を返す。 ワークフローは以下のステップを実行する。
ユーザ入力を UTF-8 にエンコードする。 ( 例:the password test )
ステップ 1 の結果の SHA-1 ハッシュを取得する。( 例: test のハッシュは A94A8FE5CC... )
最初の 5 文字(ハッシュプレフィックス)をコピーし、次の API を用いて範囲検索に使用する: http GET https://api.pwnedpasswords.com/range/A94A8
結果を繰り返し、ハッシュの残りを探す。(たとえば、 FE5CC... が返されたリストの一部であるかどうか)もしそれが返されたリストの一部でなければ、与えられたハッシュのパスワードは見つからない。そうでなければ、 FE5CC... のように、それが違反で見つかった回数を示すカウンターを返す。(例: FE5CC... : 76479 )
Pwned Passwords APIに関する詳細なドキュメントは、オンラインで確認できる。
なお、この API は、ユーザが登録やパスワードの入力を行う際に、推奨パスワードかどうかを確認するために、アプリ開発者が使用するのが最適である。
参考資料
ルールブック
4.5.3.1. ログイン制限
スロットリングの手順をソースコードで確認する:
指定されたユーザ名で短時間にログインを試行した場合のカウンターと、最大試行回数に達した後にログインを試行しない方法を提供する。認証されたログイン試行後は、エラーカウンターをリセットすること。
ブルートフォース対策として、以下のベストプラクティスを遵守する。
ログインに数回失敗したら、対象のアカウントをロックし(一時的または恒久的に)、それ以降のログイン試行を拒否すべき。
一時的なアカウントロックには、 5 分間のアカウントロックが一般的に使用される。
クライアント側の制御は簡単にバイパスされるため、制御はサーバ上で行う必要がある。
不正なログインの試行は、特定のセッションではなく、対象となるアカウントについて集計する必要がある。
その他のブルートフォース軽減技術については、 OWASP のブルートフォース攻撃の阻止のページに記載されている。
参考資料
ルールブック
4.5.4. ルールブック
4.5.4.1. 「強力な」パスワードポリシー(推奨)
以下は「強力な」パスワードを設定するためのポリシーの一例である。
パスワードの長さ
最小: 8 文字未満のパスワードは脆弱であると見なされる。
最大:一般的な最大長は 64 文字である。 long password Denial of Service attacks による攻撃を防ぐために最大長を設定することは必要である。
パスワードをユーザに通知なしで切り捨てない。
パスワードの構成規則
Unicode と空白を含むすべての文字の使用を許可する。資格情報のローテーション
パスワードの漏洩が発生、または特定されたときに資格情報のローテーションを行う必要がある。パスワード強度メーター
ユーザが複雑なパスワードを作成する、過去に特定されたパスワードの設定をブロックするためにパスワード強度メーターを用意する。
以下サンプルコードは、 Android アプリで対応すべきパスワードの長さ判定と、パスワードの構成規則の処理の一例。
レイアウトでの例:
<EditText
android:id="@+id/passwordText"
android:hint="password"
android:maxLength="64"/>
Kotlin での例:
val PASSWORD_MIN_LENGTH: Int = 8
val PASSWORD_MAX_LENGTH: Int = 64
fun validationPassword(password: String?): Boolean {
return if (password == null || password.isEmpty()) {
false
} else !(password.length < PASSWORD_MIN_LENGTH || password.length > PASSWORD_MAX_LENGTH)
}
参考資料
これに注意しない場合、以下の可能性がある。
脆弱なパスワードとなり予測される。
4.5.4.2. 推奨パスワードかの確認(推奨)
Pwned Passwords API を使用することで、ユーザが登録やパスワードの入力を行う際に、推奨パスワードかどうかを確認することができる。
これに注意しない場合、以下の可能性がある。
脆弱なパスワードとなり予測される。
4.5.4.3. ブルートフォース対策として、以下のベストプラクティスを遵守する(必須)
ブルートフォース対策として、以下のベストプラクティスを遵守する。
ログインに数回失敗したら、対象のアカウントをロックし(一時的または恒久的に)、それ以降のログイン試行を拒否すべき。
一時的なアカウントロックには、 5 分間のアカウントロックが一般的に使用される。
クライアント側の制御は簡単にバイパスされるため、制御はサーバ上で行う必要がある。
不正なログインの試行は、特定のセッションではなく、対象となるアカウントについて集計する必要がある。
その他のブルートフォース軽減技術については、 OWASP のブルートフォース攻撃の阻止のページに記載されている。
※サーバ側のルールのため、サンプルコードはなし。
これに違反する場合、以下の可能性がある。
ブルートフォース攻撃に脆弱になる。
4.6. MSTG-AUTH-6
リモートエンドポイントは過度な資格情報の送信に対する保護を実装している。
※リモートサービス側での対応に関する章であるため、本資料ではガイド記載を省略
参考資料
4.7. MSTG-AUTH-7
事前に定義された非アクティブ期間およびアクセストークンの有効期限が切れた後に、セッションはリモートエンドポイントで無効にしている。
※リモートサービス側での対応に関する章であるため、本資料ではガイド記載を省略
参考資料
4.8. MSTG-AUTH-12
認可モデルはリモートエンドポイントで定義および実施されている。
※リモートサービス側での対応に関する章であるため、本資料ではガイド記載を省略