Open29

認証・セキリュティまわり

Kazuki MatsudaKazuki Matsuda

トークン認証・セッション認証について改めて基礎から学び直すスクラップ。
有益な記事がたくさんあるので、適当に読み漁り、自分が見返すように要点をコメントするのがメイン。

ベースはこちらブログ

https://koduki.hatenablog.com/entry/2019/11/03/163014

当たり前ですがJWTであるアクセストークンは改ざんチェックしかしてないので本人認証としては弱い。なんらかの問題で流出したらアウト。秘密鍵を変えるか有効期限が過ぎるのを待つしか無いです。

たとえリフレッシュトークンを使って再発行しても古いアクセストークンをステートレスに無効にすることは出来ない。やるならブラックリストデータを状態として持つしかない。

なので理想的にはアクセストークンはワンタイムトークンとして振る舞うのがセキュリティ的には良いと思います。仕様上それは出来ないから1分以下とか超短期の有効期限にするのがベスト。

これならアクセストークンが流出してもあまり問題はないはず。リフレッシュトークンはサーバサイドで状態を持ってるので万一流出したら破棄することが出来るので通常のセッションと同程度の長さで問題はないし明示的なログアウトも作れます。

ただし、アクセストークンの有効期限が短いと認可サーバへの問い合わせ回数が増えるのでここはトレードオフですね。認可サーバの負荷的には従来のセッションによると大差無い気もするけどクライアント側の通信コストが問題。ユーザが多いと署名コストもボトルネックになるかもです。

この辺のさじ加減は作ってみないと分からないのでバランスはサービス次第になるかと思います。

Kazuki MatsudaKazuki Matsuda

https://co3k.org/blog/why-do-you-use-jwt-for-session

JWTをセッションで使う場合の問題点として挙げられている項目

  • サーバー側でセッションの無効化ができない
    • 秘密鍵を更新すれば、発行済みの全セッションを無効することは可能
  • ログアウトしてもユーザーのストレージからトークンが削除されるだけであり、トークンそのものが無効になるわけではない
    • CSRF 対策にはなるが、セッションハイジャックは防げない
  • 有効期限を明示的にペイロードに含まない限り、セッションが恒久的に有効になる
  • 秘密鍵を知っていれば任意のユーザに対するセッションハイジャックが可能
  • 秘密鍵は定期的に更新しなければならない
  • JWT 特有の罠がある
    • alg の柔軟性 (alg=none 許容で即死など)
    • JWE で利用可能な暗号アルゴリズムが微妙 (らしい)
  • トークンのサイズが大きくなる場合があり、 cookie にデータを保存しにくい
    • Web Storage API を利用
      • cookie の HttpOnly フラグの保護を受けない
      • cookie の secure フラグの保護を受けない

この記事へのアンサー記事

https://auth0.hatenablog.com/entry/2018/09/21/004131

JWT認証のメリットはその実装のシンプルさとステートレスなことにあります。現実的には実際はDB参照とか必要になったりするんですが、ほとんど改ざん検証だけで済むのは魅力的です。トレードオフでリアルタイムでユーザー無効化ができないことくらいですかね。ライブラリなんて使う必要ないほどシンプルだし、トレードオフさえ許容できればむしろ、なぜこれ以上に複雑な認証システム使わないといけないの?複雑さゆえにライブラリが必要になったり、そのライブラリが脆弱性を抱えていたり、そもそも使い方を間違えてしまったりするんでしょう。

Kazuki MatsudaKazuki Matsuda

https://qiita.com/hakaicode/items/1d504a728156cf54b3f8

  • 明示的にログアウトするにはサーバー側の秘密鍵の変更が必要
    • 秘密鍵を変更した場合
      • 誰か一人でもログアウトしたいときは全員ログアウトになる
    • 秘密鍵を変更しない場合
      • なりすましログインが発覚しても、トークンの有効期限が来るまではそのセッションは無効にできない
  • ↑ の回避策
    • 明示的にログアウトしたユーザーを無効なトークンとして管理する
    • ユーザーがパスワードを変えたらトークンも再発行の必要があるようにする
      • 結局のところサーバーサイドでのステートフルなセッション管理
      • JWTのメリットであるステートレスを捨てている
Kazuki MatsudaKazuki Matsuda

https://zenn.dev/ritou/articles/4a5d6597a5f250

コメントからそのまま記載

多分もう見ていないと思いますし、私が答える事でもないと思いますが、似たことを考えて実装をしたことがあるので…

"内部でセッションIDを無効化することでJWT自体も無効化可能"

この点については文字通りの意味だと思います。
まず、今まで通りのセッションIDを考えてください。
サーバサイドでデータストア(RDBMS,Memcache,Redis等)でセッションIDとユーザを紐づけることをやっていると思います。
なので、セッションIDをデータストア上から削除すれば無効化できますよね。

JWTは「署名をつけることもできる規格化された入れ物」です。なのでJWTの中にセッションIDを持つことは可能です。
JWTからセッションIDを取り出してデータストアに突き合せて使うことができます。そのセッションID自体は上記の操作で無効化できます。ただそれだけだと思います。

わざわざJWTで包むと何がうれしいのかというと、書かれている通りなんですが、JWT(正確にはJWSによる署名を含む)により「(署名の鍵が盗まれていない限り)JWTを発行した人が信頼できること」「内容の改ざんがないこと」「JWTによる期限」といったことが検証できます。
これらはデータストアに問い合わせることなく可能なので、場合によっては負荷が軽減します。署名検証分の負荷は増えますが、ネットワークレイテンシと比べたら些細です。また、悪意あるユーザからの攻撃に対してデータストアの負荷が軽減するといった効果があります。(セッションIDだけではとりあえずデータストアに突き合せないといけないですからね。)

また、セッションIDは作り方が課題です。
セッションIDには乱数が用いられたりしますが、脆弱な実装だと乱数を推測できたり固定化することができてしまいます。特にOSSなフレームワークを使っていると分かると突かれることがあります。
極端な話ですがセッションIDを適当に作っていたらログイン出来ちゃったみたいなことも考えられます。
(もちろん、今はほとんどフレームワークが強固な方法で用意してくれており、メジャーな対策は含めているはずなので気にすることはあまりないかもしれません)

そんなセッションIDをJWTで包んでやると署名がつくので、HMAC等の署名技術が破られない限りそういった攻撃は困難です。
(署名も適当に作ったら通っちゃったみたいなケースは考えられますが、セッションIDと署名を同時に当てるのはセッションIDをあてるよりも絶対に困難だと思います。もちろん署名の作り方にもよりますが脆弱な方法を使わないといった一般的な話として・・・)

それだと攻撃者によって改ざん前にJWTがコピーされていた場合に無力だなと思いました

その通りです。
これはセッションハイジャックであり、セッションIDだろうがJWTで包もうが、秘匿されるべき情報が盗まれてはどうしようもありません。
これに耐性がある仕組みは存在しないので、セッションハイジャックは起きないように実装すべきでしょう。

なお、JWTに利用者のIPv4を埋め込むことで、通信者のIPとJWT内のIPが一致しないと通さないといった実装もできますが、IPは変わりますからセッション管理に使うには厳しいでしょう(他のユースケースでは使えるかもしれません)

Kazuki MatsudaKazuki Matsuda

https://ritou.hatenablog.com/entry/2020/06/08/050000

JWT とは

  • JWTはとにかく色々なデータ、例えば構造化されたものからバイナリデータまでを複数のサービス、システム間でやりとりするため、URLセーフな文字列にエンコードする仕組みです。 また、そのエンコード結果の文字列自体をJWTと呼ぶこともあります。
  • さらに、JSON Web Signatureという署名をつける仕組みを利用することで改ざん検知が可能になります。JSON Web Encryptionという暗号化を施すことにより、センシティブなデータのやりとりが可能になります。 この2つのうち、よく使われているのを見かけるのがJSON Web Signatureの方でしょう。
  • Google、Appleでサインインの裏側で動いているOpenID Connectの中で、ID Tokenというユーザーの属性情報や認証時の情報などをやり取りする際にJWTが利用されています。
  • もともとSAMLというXMLベースのID連携の仕組みで使われていたXML署名という仕組みがあらゆるユースケースに対応できるようにした結果複雑になってしまったという経緯から、JWTはより容易に実装できてコンパクトに表現できるセキュリティトークンを目指しました。
Kazuki MatsudaKazuki Matsuda

https://ritou.hatenablog.com/entry/2019/12/01/060000

JWT は次の特徴を持つエンコードフォーマットです。

  • 様々なデータをURL Safeにエンコードできる
  • JWS(RFC7515) で署名をつけたり、JWE(RFC7516) で暗号化したりできる
  • 構造化されたデータをやりとりするための標準的な claim が定義されている(RFC7519)
  • 署名や暗号化のアルゴリズム(RFC7518)、鍵の表現(RFC7517)も豊富
Kazuki MatsudaKazuki Matsuda

https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be

アクセストークンの要求方法とそれに対する応答方法を標準化したものが OAuth 2.0 である

https://qiita.com/TakahikoKawasaki/items/498ca08bbfcc341691fe

OAuth 2.0 はアクセストークンを発行するための処理フローを定めていますが、それを流用し、ID トークンも発行できるようにしたのが OpenID Connect なのです。

そもそも ID トークンは何のためにあるのでしょうか? それは、ユーザーが認証されたという事実とそのユーザーの属性情報を、捏造されていないことを確認可能な方法で、各所に引き回すためです。 一ヶ所で(=一つの OpenID プロバイダーで)ユーザー認証をおこない、発行された ID トークンを引き回すことができれば、別の場所で何度もユーザー認証を繰り返す必要がなくなります。 短く言うと、『ID 連携』ができます。

Kazuki MatsudaKazuki Matsuda

https://techracho.bpsinc.jp/hachi8833/2019_10_09/80851

結論としては、XSS を 100% 防ぐことはできないよね、という内容。
ただ、LocalStorage だろうと、Cookie だろうと、XSS された時点でどうしようもない気がする...。
確かに Cookie の場合は、HttpOnly フラグをつければ、JS からはアクセスできないが、トークンを使った操作はできる。トークンが盗まれることよりも、こっちが本題。

Kazuki MatsudaKazuki Matsuda

https://zenn.dev/musou1500/articles/6bc49fe0f9d7ed7e20cc

同じようなことが書かれている。

攻撃者にとって真に重要なのは認証トークンでは無く,認証トークンを使って何をするか,ということのはずだ.

つまり,ペイロードが評価されたと同時に攻撃する方が効率的だし,この場合クッキーは送信されればよく,読む必要すら無い.このことを踏まえると,XSSが発生した場合,クッキーのHttpOnly属性が効果的だとも思えない.

Kazuki MatsudaKazuki Matsuda

超入門:ウェブサイトのパスワード保護~ウェブサイトでパスワードを保護する方法~

  • パスワードは暗号化ではなくハッシュ化されることが多い理由
    • 暗号化の場合、鍵の管理が難しい
    • パスワードをサイト管理者にも知られたくない
  • ただのハッシュ化の場合、同じパスワードだとハッシュ値も同じになる問題がある
    • ソルトによる対策
    • ソルトとはパスワードに追加する文字列
    • 当然ソルトはユーザーごとに変える
    • ソルトは通常ハッシュ値と一緒に保存
  • ストレッチング
    • ハッシュの計算を繰り返すこと
    • 十分長いパスワードの場合、ストレッチングは必要ない
    • 弱いパスワードの救済
  • ペッパー
    • パスワード情報が漏洩する状況では、ハッシュ値とともにソルトも漏洩する
    • そこで、パスワードにペッパー (固定の秘密文字列) を追加してからハッシュ値を求める
    • ペッパーは DB 以外の場所に保存
Kazuki MatsudaKazuki Matsuda

CORSの原理を知って正しく使おう

  • 同一オリジンポリシーとは
    • オリジンとは
      • スキーム + ホスト + ポート
      • スキーム: http
      • ホスト: example.com
      • ポート: 80
    • 他のオリジンからリソースにアクセスできないように制限するもの
  • CORS
    • クロスオリジンのリソースアクセスを提供

今日こそ理解するCORS

  • オリジンを信頼する・しないは、サーバーがブラウザに Access-Control-Allow-XX ヘッダを用いて伝える
  • CORS の保護戦略
    • リクエストを送る前にお伺いを立てる = プリフライトリクエスト
    • 帰ってきたレスポンスを JS に渡して良いことを許可する
  • プリフライトリクエスト
    • リクエストの内容からブラウザが自動的に API サーバーに許可を求める

Kazuki MatsudaKazuki Matsuda

https://kurochan-note.hatenablog.jp/entry/2022/04/18/112307

ステートフルなセッション識別子は、ログアウト後にはサーバー上で無効とされるべきである。ステートレスなJWTトークンは、攻撃者の機会を最小にするために、むしろ短命であるべきである。寿命の長いJWTの場合は、アクセスを取り消すためにOAuth標準に従うことが強く推奨される。

Kazuki MatsudaKazuki Matsuda

https://developer.mamezou-tech.com/blogs/2022/12/08/jwt-auth/

JWTの説明と一緒に良く出てくる認証や暗号化の話は、ここまで一切でてきていません。このことからも分かるようにJWT自体は認証や暗号化に関してなにも規定していません。単にスペースに制約のあるHTTPヘッダーにJSONを載せるために、URLセーフでデータを小さくするJSONのデータフォーマット(表現形式)を決めているだけです。

後ほど出てくる話ですが、参考までに先にお伝えしてくと、JWTと認証の関係は、アプリケーション間で認証データをやり取りする伝達手段としてJWTが都合が良いために使われているだけで、JWTと認証は本来なんら関係はありません。

JWSではこのようにシグニチャを復号化した結果とヘッダーとペイロードを比較し、改ざんされていないかを検証します。

Kazuki MatsudaKazuki Matsuda

https://speakerdeck.com/hiro_y/update-your-knowledge-of-csrf-protection

  • 防御方法
    • トークン
    • SameSite 属性を指定した Cookie
      • Strict: 同一サイトのみ送信
      • Lax: 異なるサイト間の場合、GET の場合だけ送信
      • None: どんな場合でも送信
      • SameSite 属性利用のデメリット
        • 古いブラウザが対応していない
        • GET の場合は Cookie が送られてしまう (Lax)
          • GET で状態変更できることがよくない
        • Cookie を利用しない場合は使えない
          • 認証不要な場合など
      • Origin ヘッダー
        • リクエストの Origin ヘッダーの値とサーバー側のホスト情報を照合
      • Sec-Fetch-* ヘッダー
        • Sec-Fetch-Dest: fethc するリソースを使う形態。document や image、font...
        • Sec-Fetch-Mode: リクエスト形態。navigate、cors...
        • Sec-Fetch-Site: リクエスト元 Origin との関係。same-origin、corss-site...
Kazuki MatsudaKazuki Matsuda

https://blog.jxck.io/entries/2024-04-26/csrf.html

app.use((req, res, next) => {
  // post である場合は origin と sec-fetch-site をチェック
  if (req.method === "post") {
    // origin は必ずチェック
    if (req.headers.origin !== "https://sns.example") {
      return res.send(400)
    }
    // sec-fetch-site は、存在した場合だけチェック
    if (req.headers.secFetchSite && req.headers.secFetchSite !== "same-origin") {
      return res.send(400)
    }
  }
  return next()
})

// デフォルトに頼らず Cookie に Lax を明示
// 理想は read と write に cookie を分け write を Strict にする
app.use(session("Lax"))

// 副作用のある API は必ず POST にする
app.post("/post", async (req, res) => {
  await createPost(req.body)
  // ...
})