はじめに
RustのWebフレームワークの一つであるactix-webを使って発行されたセッションCookieの中身を調べてみました。
本記事ではactix-webと組み合わせて使うactix-sessionのオブジェクトを使ってキーと値を生成し、その情報をセッションCookieに格納してクライアントに返す実装例を記載します。クライアントがセッションCookieを受け取った後、そこからactix-sessionで作ったキーと値を復元するのが目標です。
過去にPythonのFlaskで同様のことを行っており以下記事に書いていますが、今回はそのRust版です。
動作確認バージョン
$ rustc -V
rustc 1.73.0 (cc66ad468 2023-10-03)
$ cargo -V
cargo 1.73.0 (9c4383fb5 2023-08-26)
Webサーバの作成
まずはセッションCookieを生成するWebサーバを作ります。
Cargo.tomlにパッケージ追記
actix-webとactix-sessionのパッケージが必要なため、Cargo.toml
の[dependencies]
セクションに以下のように記載します。
[package]
name = "actix-web-cookie-test"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.6.0"
actix-session = { version = "0.8.0", features=["cookie-session"] }
※ 今回セッションCookieを発行するのにactix-session::storage::CookieSessionStoreを使うため、cookie-session
のfeatureが追加で必要となります。
Webサーバ実装
次の機能を持つWebサーバを実装します。
- Webクライアントが「user_idのキーを格納したセッションCookie」を持っているかを確認
- 持っている場合は何もしない
- 持っていなければ、キーがuser_idで値が1のセッションCookieを発行する
以下がそのソースです。
use actix_web::{ cookie::Key, App, HttpServer, get, HttpResponse };
use actix_session::{ Session, SessionMiddleware, storage::CookieSessionStore };
use actix_session::config::CookieContentSecurity::Private;
/// セッションCookieを発行するWebサーバのpathとその処理を記載
#[get("/")]
async fn root(
session: Session,
) -> HttpResponse {
let mut response_text = String::new();
if let Some(_) = session.get::<String>("user_id").unwrap() {
response_text.push_str("Session cookie accepted.\n");
} else {
session.insert("user_id", "1").unwrap();
response_text.push_str("No user_id key in session.\nSession cookie created.\n");
}
HttpResponse::Ok().body(response_text)
}
/// セッションCookieの作り方について記載(後で解説)
fn session_middleware(
secret_key: &str,
) -> SessionMiddleware<CookieSessionStore> {
SessionMiddleware::builder(
CookieSessionStore::default(),
Key::from(secret_key.as_bytes()),
)
.cookie_name(String::from("mycookie"))
.cookie_content_security(Private)
.cookie_secure(false)
.build()
}
#[actix_web::main]
async fn main() -> Result<(), actix_web::Error> {
// セッションCookieを作成する際のシークレットキー。64バイト以上である必要あり
let secret_key = "secret_key".repeat(7);
HttpServer::new(move ||
App::new()
.wrap(session_middleware(&secret_key)) // 後で解説
.service(root)
)
.bind("0.0.0.0:3000")?
.run()
.await?;
Ok(())
}
actix-webのミドルウェアを作成している部分についての解説
session_middleware
関数では、セッションCookieを生成するためのミドルウェアであるSessionMiddlewareのオブジェクトを作成しています。SessionMiddlewareオブジェクトは、actix-webのHttpServerでWebサーバを起動する際にwrap関数によって組み込まれます。
このactix-webのミドルウェアが具体的に何なのか?私は最初ピンと来なかったのですが、調べていくと、リクエストやレスポンスをする際に追加の処理を入れられるものだということがわかりました。公式ドキュメントは以下です。
今回の場合だとSessionMiddlewareのオブジェクトが、セッションCookieを作成したり、リクエストで届いたセッションCookieを検証したりするところを一手に引き受けてくれます。 SessionMiddlewareが具体的にどのような処理をしているのかは、以下のソースに書いてあるようです。ご興味のある方はぜひ解析してみてください。
SessionMiddlewareが色々とやってくれるので、我々が実装するWebサーバのソースではセッションCookieを作るために必要な情報だけ渡せば済みます。上記のmain.rs
のソースだとroot
関数内のsession
というオブジェクト(actix_session::Session型)がこれに相当して、セッションCookieに含めるキーと値を格納しています。SessionMiddlewareはこのsession
に格納された情報をもとにクライアントに返すセッションCookieを作ったり、クライアントから受け取ったセッションCookieから自分で作ったキーと値を抽出してsession
に格納する処理を行ったりしています。
以前に書いたPython FlaskのセッションCookie調査の場合、セッションCookieを生成する機能がFlask自体にsessionオブジェクトとして提供されていました。しかしRustのactix-webには、既にお察しのようにそのような機能がありません。代わりにactix-sessionが、セッションCookieをいい感じで生成してくれます。
SessionMiddlewareBuilderについて
SessionMiddlewareのオブジェクトは、SessionMiddlewareBuilderを使って作られ、セッションCookieの作成に関する様々な設定が行えます。本記事のソース main.rs
では、次の設定を行っています。
.cookie_name(String::from("mycookie")) // Cookie名を"mycookie"にする
.cookie_content_security(Private) // セッションCookieを暗号化する
.cookie_secure(false) // httpのリクエストでもセッションCookieを作れるようにする
Webサーバを起動して、クライアントからセッションCookieを取得できることを確認
cargo run
コマンド等でWebサーバを起動します。
Webサーバ起動後にクライアントからcurl
を実行すると、次のようにセッションCookieが送られているのを確認できます。
$ curl -i http://127.0.0.1:3000/
HTTP/1.1 200 OK
content-length: 51
set-cookie: mycookie=B%2FujNXu7ZsV+2TTriWdxSpwu1+9Fd%2FODUdEBDny+4Bzcey0axtqGvZEGT+D6jWU%3D; HttpOnly; SameSite=Lax; Path=/
date: Sat, 25 May 2024 06:36:44 GMT
No user_id key in session.
Session cookie created.
余談 : CookieContentSecurityの設定を変えた場合の挙動
SessionMiddlewareBuilderのcookie_content_security
関数で、引数をPrivate
ではなくSigned
とした場合、次のようにセッションCookieにuser_id:1
のキーと値が暗号化されずに直接格納されます。
$ curl -i http://127.0.0.1:3000/ 2>&1 | grep ^set-cookie
set-cookie: mycookie=7pwihAkZObaaW26QBIpMi5OYp2WP2aFUBx7Q%2FCWSQek%3D%7B%22user_id%22%3A%22%5C%221%5C%22%22%7D; HttpOnly; SameSite=Lax; Path=/
以下のCookieContentSecurityにも書かれていますが、セッションCookieの内容を秘匿化したい場合はPrivate
、中身がクライアントに見えても問題ないので改ざんを防ぎたい場合はSigned
で署名をする、という使い分けをするようです。
※さらに余談になってしまいますが、セッションCookieにはセッション維持のために必要な最低限の情報を格納し、それ以外のセッションに関する情報はRedis等を使ってサーバー側に持たせる「サーバーサイドセッション管理」という方法もあります。この場合だと要件にもよると思いますが、秘匿化はせずに署名のみで事足りるケースもあるかもしれません。
セッションCookieの中身を調査
先ほどcurlコマンドで、CookieContentSecurity
がPrivate
の場合に出力されたセッションCookieの値は以下でした。
B%2FujNXu7ZsV+2TTriWdxSpwu1+9Fd%2FODUdEBDny+4Bzcey0axtqGvZEGT+D6jWU%3D
パッと見る限り少なくともURLエンコードされているのはわかりますが、それ以外はどうなっているのかさっぱりわからないです(暗号化されているので当然ですが...)。以前にPython FlaskのセッションCookie調査でやったようなヤマ勘はとても無理そう。一体どうすればこの中身がわかるのだろうか...
先ほど、SessionMiddlewareの機能の一つとして、「クライアントが送られたセッションCookieから自前で作成したキーと値を抽出する」ものがあると述べました。ということは、SessionMiddlewareのソースを参考に実装したら、上の不可解な文字列から意味のある情報を抽出できそうです。
改めてソースを見てみると、extract_session_keyというドンピシャな名前の関数がありました。これをもとに実装してみたのが以下です。
use cookie::{Cookie, Key, CookieJar};
fn main() {
// curlのset-cookieの結果をそのままコピペ
let cookie_str = "mycookie=B%2FujNXu7ZsV+2TTriWdxSpwu1+9Fd%2FODUdEBDny+4Bzcey0axtqGvZEGT+D6jWU%3D; HttpOnly; SameSite=Lax; Path=/";
let cookie = Cookie::parse_encoded(cookie_str).unwrap();
let secret_key_str = "secret_key".repeat(7);
let secret_key = Key::from(secret_key_str.as_bytes());
let mut jar = CookieJar::new();
jar.add_original(cookie.clone());
// secret_keyでmycookieという名前のセッションCookieを復号
let private_cookie = jar.private(&secret_key).get("mycookie");
println!("{:?}", private_cookie.unwrap().value());
}
上記のソースを動かすには、Cargo.toml
で以下のようにパッケージを記載する必要があります。
[package]
name = "extract-session-cookie"
version = "0.1.0"
edition = "2021"
[dependencies]
cookie = { version = "0.16.0", features=["private", "percent-encode"] }
ビルドして実行すると、次のようにサーバ側で格納した"user_id"
と"1"
のキーと値を見事に取得できました!
$ cargo run
Compiling extract-session-cookie v0.1.0 (/path/to/extract-session-cookie)
Finished dev [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/extract-session-cookie`
"{\"user_id\":\"\\\"1\\\"\"}"
ちなみに、セッションCookieを復号する以下に関してですが、
// secret_keyでmycookieという名前のセッションCookieを復号
let private_cookie = jar.private(&secret_key).get("mycookie");
以下のソースに暗号化(encrypt_cookie)と復号(unseal)の処理が書かれていました。