この記事では最小構成のログインフォームを1つ用意し、送信されたパケットをWiresharkで開いてパスワードがそのまま読み取れることを確認します。その後フォームをHTTPSに切り替え同じ操作で中身が暗号文に変わることを確かめます。
こんな感じのフォームで検証します!
ユザネとパスワードはこんな感じ!
ユーザー名: alice
パスワード: himitsu1234
今回確かめること!
- HTTPは通信の中身を暗号化せずに流している。HTTPSにすると暗号文になる。
今回はキャプチャを自分のPCの内側で行います。
送信も受信も観測も自分1台で完結しており他人の通信を盗み見る状況は再現していません。
検証用アプリの構成
先ほど載っけたようなVueで作ったフォームを用意します。
.
├── index.html
├── src/
│ ├── main.js
│ └── App.vue
└── vite.config.js
フォームの送信部分↓
async function submit() {
const res = await fetch("/api/login", {
method: "POST",
body: new URLSearchParams({ username: username.value, password: password.value }),
});
result.value = await res.text();
}
fetchのbodyにURLSearchParamsを渡しているのがポイントです。こうするとContent-Typeがapplication/x-www-form-urlencodedになり、送信される中身はusername=alice&password=himitsu1234のような素朴なキーと値の並びになります。Wiresharkはこの形式を認識してキーと値に分解して表示してくれるので分かりやすいです。
送信先の/api/loginは、ViteのconfigureServerで受け止めて、受け取った内容をそのままJSONで返します。
function fakeLoginApi() {
return {
name: "fake-login-api",
configureServer(server) {
server.middlewares.use("/api/login", (req, res) => {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify({ ok: true, received: Object.fromEntries(new URLSearchParams(body)) }));
});
});
},
};
}
HTTPとHTTPSはViteのモードで切り替えます。HTTPSのときだけ@vitejs/plugin-basic-sslで自己署名証明書を有効にしてポートも分けます。
export default defineConfig(({ mode }) => {
const https = mode === "https";
return {
plugins: [vue(), fakeLoginApi(), https && basicSsl()],
server: { port: https ? 8443 : 8080, strictPort: true },
};
});
ポート番号について
Viteのポートを8080番に固定しています。Wiresharkはポート番号を手がかりにパケットをHTTPとして解釈しますがその自動対象に5173番は含まれていません。5173番のまま進めるとパケット自体は流れているのにhttpフィルタに何も出てこなくなりますので自動対象に入っている80番や8080番に変更します。strictPortは8080番が埋まっていたときに別ポートへ勝手に逃げる挙動を止めるためのものです。
HTTPS側の8443番にはこうした制約はありません。WiresharkはTLSをポート番号ではなくパケットの中身から判定するのでどのポートでもtlsフィルタに出てきます。8443番はHTTPSの代替ポートとして慣例的によく使われる番号というだけで8080番と区別できれば他の番号でも構いません。
HTTPで平文が見えることを確認する
-
Wiresharkを起動します。最初の画面に出るインターフェース一覧から
Loopback: lo0をダブルクリックしてキャプチャを開始します。一覧の上にある緑色の入力欄(キャプチャフィルタ)には何も入れません。 -
キャプチャが始まると画面上部に表示フィルタの欄が現れます。そこへ次の式を半角で入力してEnterを押します。
http.request.method == "POST" -
ターミナルで依存を入れて開発サーバーを起動し、ブラウザで
http://localhost:8080を開きます。npm install npm run dev -
フォームのログインボタンを押します。
-
Wiresharkに
POST /api/loginの行が1つ現れます。

その行を選び、下部の詳細ペインで
HTML Form URL Encodedの項目を展開します。
usernameとpasswordが値ごと並んでいるのがわかります。

-
全体を1画面で見たいときは、その行を右クリックして
追跡(Follow)からHTTP Streamを開きます。HTTP Streamで見えるのは、おおむね次のような中身です。POST /api/login HTTP/1.1 Host: localhost:8080 Content-Type: application/x-www-form-urlencoded;charset=UTF-8 Content-Length: 35 Origin: http://localhost:8080 username=alice&password=himitsu1234 HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 {"ok":true,"received":{"username":"alice","password":"himitsu1234"}}リクエストの最後の行に、入力したユーザー名とパスワードがそのまま乗っているのがわかります。
HTTPSで同じことをして見比べる
次に同じフォームをHTTPSで送って同じ操作で中身がどう変わるかを見てみます。
-
Wiresharkの表示フィルタを次に変えます。
tls -
HTTPSモードで開発サーバーを起動しブラウザで
https://localhost:8443を開きます。自己署名証明書なので警告が出るかもしれませんが詳細を開いて続行します。npm run dev:https -
同じフォームでログインボタンを押します。
-
Wiresharkには
Application Dataと表示されたパケットが並びます。

中身を展開しても表示されるのは暗号化されたバイト列で
himitsu1234はもちろんpasswordという語も見つかりません。

-
その行を右クリックして
追跡(Follow)からTCP Streamを開いても同じですね。読める文字はTLSのやり取りに含まれるlocalhost程度でフォームの中身は現れませんでした。
HTTPでまる見えだったパスワードがHTTPSでは暗号文になることがわかりました!
最後に
Wireshark、本当にいろんな事ができそうなのでこういう実際に体験してみる系の検証やっていきたいなと思いました。
