この記事は、上智大学エレクトロニクス研究部Advent Calendar第6日目の記事です。
まえがき
今日のお題は確率的に発生するバグです。入力を受け付けたり、ランダムに値を生成するプログラムを作っていると、手元では動いたのに別環境にデプロイした後バグが発覚する事例があります。この記事では実際に僕が実装した例を紹介いたします。
どんなものを作っていたか
個人サイトのアプリケーションでLambda(とDynamoDB)を使ってセッション管理をしようとしていました。普段であれば、FlaskなどのWebフレームワークを使っているのでセッション管理もライブラリを使っていますが、今回はLambda上の実行ということで、極力外部モジュールを使わず自力で実装しました。
そこで、NodeJSの標準モジュールである crypto
の randomBytes
メソッドを使いランダムなバイト列を作り、それをURL安全な形にするために base64
でエンコードすることにしました。
最初に書いたコードは以下の通りです。
import {APIGatewayProxyHandler} from "aws-lambda";
import * as crypto from "crypto";
export const hello: APIGatewayProxyHandler = async (_event, _context) => {
const sessionId = crypto.randomBytes(24).toString("base64");
const setCookie = `sessionId=${sessionId}; HttpOnly; Secure`
return {
statusCode: 200,
body: "hello, world!",
headers: httpUtils.toKebabCase({setCookie})
};
};
何が問題だったか
base64
で完璧にエスケープできていると思いこんでいたのが間違いだったのです。 base64
はバイト列を印字可能な文字列に変換するために、a-zA-Z0-9/+
の64種類の文字を使います。しかし、この中に1文字だけ Set-Cookie
ヘッダの値にそのまま使うとまずい文字が紛れています。そう、+
が問題の文字です。NodeJS(に限らずサーバー)は値を処理するときに、 +
を発見すると空白文字として処理してしまうのです。それを回避するには、+
を %2b
に置き換えてあげる必要があったのです。
教訓
- しっかりとエスケープを行う
- 動作確認を何回も行う。テストケースを書く。
あとがき
最後になぜ40%の確率で失敗するのかを計算で示します。
上記のコードの sessionId
は24バイトのものをbase64でエンコードしているので、 24 * 4 / 3 = 32(文字)
となっています。32文字のうち1文字も +
が含まれない確率は (63/64) ** 32 ~ 0.604 = 60%
。逆に含まれてしまう確率は 40%
であることがわかります。
この記事以外にも上智大学エレクトロニクス研究部の部員が面白い記事をたくさん書いているので、ぜひ読んでみてください!