ランダムな文字列のURLを認証代わりにつかって良いか?

Q.
UX的にログインしたくない。認証の代わりにランダムな文字列URLで代用できないか?また、その場合は何文字のURLなら可能か?

 

A.

有効期限がある一時的なURLとして利用ならOK。永続的なURLとして実施の場合はNG。
文字数はその時に有効なコンテンツの数次第。
心配であれば、base64で22文字(128bit)以上で設計すればよい。
ちなみに、google photoの写真シェアは英数大小 17文字(log_2(62^17) ≒ 101bit)。

===============================


 1.認証の代わりにつかってよいか?

ランダム文字なURLは、W3CではCapability URLsと呼ばれている。 

 https://www.w3.org/TR/capability-urls/

使う注意点としては

  • httpsを使う
  • 期限を設ける
  • Referrerをしっかり(Referrer-PolicyヘッダーでURLのパス情報が漏れないように)
  • 検索エンジンにクロールされないようにする
  • サードパーティーのwebサイトや信頼できないサードパーティースクリプトリンクをページに含めない
  • URLが認証されたユーザで制御されている場合、そのユーザによってURLの無効化ができること
  • アドレスバーを書き換えてURLが表示されない対策など
  • 他ユーザと共有する機能を提供する場合、そのURLが広範囲に共有された場合の影響を伝えること

 

URLはページの共有などユーザが意図しないところで漏れる可能性も高く、ログインと同等を求めるにはリスクが高いため、基本的にはNGがこのましい。

(クレジット番号があるのに、URL知っていれば見れちゃうとなれば、攻撃者はawscurlスクリプトつくって検知されづらいように複数台でのんびりクロールでもするでしょう。)

 

ただし、そもそもそれってログイン必要な機能なのか?という観点でサービスを整理することが重要。

 

たとえば、google photoだと ログインは不要で、リンクを知っていれば誰でも見れるとしている。(共有リンクの無効化も可能)

f:id:aiaru:20210107153445p:plain

 

2.URLの文字数について

文字数を長くする目的はブルートフォース攻撃に耐えること。

そして、これはその時アクティブなコンテンツの量に応じて影響。

たとえば、URLを2文字(2byte=16bit)だと、これで表せるURLは65,536通り。

コンテンツが33,000あったら、1/2の確率でコンテンツに当たってしまうので、これだと文字列が少ないわけ。なので、文字数はある程度長い文字列が必要。

社内への説明などあると思うが、個人的には17文字、22文字あたりが安全で説明もしやすいのでおすすめ。

■17文字の根拠

大抵の企業は、「あのgoogleが!!」と言う言葉に弱い。

なので、「あのgoogleの写真をシェアする機能では17文字を採用しているので、17文字とする!」で社内説明がつくのであれば、これでもいいかと。

確率は2.9*10^30...まぁブルートフォース攻撃は成り立たないだろう。

f:id:aiaru:20210107162555p:plain

 

■22文字の根拠

重複しないIDとして有名なUUID。

「UUIDと同等の強度です」で社内説明がつくのであれば、これでもいいかと。

UUIDは128bit。base64 URL Safeでエンコードするとすると22文字(128/6 ≒ 21.33 ->22文字)※v4の場合、6bit固定なので122bit.

確率は3.4*10^38...まぁブルートフォース攻撃は成り立たないだろう。

 

f:id:aiaru:20210107163932p:plain


3.URLの文字の生成方法は?

結論から言うと、SecureRandom(またはRandom)でよいかと。

■SecureRandom

ランダムを使って文字列を作成。

SecureRandomの方が安全ではあるが、Linuxでは標準で /dev/random が利用されてしまうため、エントロピーが消費されてしまうとブロッキングされてしまう問題がある。

これを気にするのであれば、/dev/urandomが利用されるRandomを使えば良い。

Java8移行だと、
JEP123 で利用時に新しいアルゴリズムが定義され、ブロッキングされずに利用できる方式が提供されているので、対象の言語で安全なランダム値を使えばよい。(内部的にはほとんどのOSで/dev/urandomが利用されているので、Randomと大差ないが。。。)

#ソースイメージ

//SecureRandom random = SecureRandom.getInstanceStrong();
//ほとんどの環境ではInstanceStrongでブロッキングアルゴリズム
//NativePRNGBlockingが選択される。
//ブロッキングしたくない場合は明示的にノンブロックングのNativePRNGNonBlocking
//を指定してインスタンス生成するとよい。
SecureRandom random = SecureRandom.getInstance("NativePRNGNonBlocking");

byte[] buffer = new byte[16];//12bit = 16byte
random.nextBytes(buffer);

String randomURL = Base64.getUrlEncoder().encodeToString(buffer);

 

■UID

UUIDの実装が大抵の場合はSecureRandomなのでUUIDを直接使うメリットはあまりない。社内政治のせいで使わざる追えない場合は使えばよい。

ちなみにUUIDをそのまま使うと以下のように32文字となる。

1AWSAE12-2D5D-4A81-B709-3A45BFEFE12F 

文字数を減らしたい場合は、以下のようにUUIDをBase64に変えてあげれば良い。
 

#ソースイメージ

long msBits = uuid.getMostSignificantBits();
long lsBits = uuid.getLeastSignificantBits();
byte[] buffer = new byte[16];
for (int i = 0; i < 8; i++) {
buffer[i] = (byte) (msBits >>> 8 * (7 - i));
}
for (int i = 8; i < 16; i++) {
buffer[i] = (byte) (lsBits >>> 8 * (7 - i));
}

//--- または ------
long tmpLong =uuid.getMostSignificantBits();
byte[] msBytes = ByteBuffer.allocate(8).putLong(tmpLong).array();

tmpLong =uuid.getLeastSignificantBits();
byte[] lsBytes =ByteBuffer.allocate(8).putLong(tmpLong).array();
byte[] buffer = new byte[16];
#byte[]結合
System.arraycopy(msBytes,0,buffer,0 ,rmsBytes.length);
System.arraycopy(lsBytes,0,buffer,msBytes.length,lsBytes.length);