Help us understand the problem. What is going on with this article?

WebSocketでチャットを作ったらN高生たちにめちゃくちゃに壊された件

はじめに

N高等学校2年のKakudoです、初めてアドベントカレンダーを書かせていただきます。よろしくお願いします。

さて、WebSocketを使ってみようと簡単なWebアプリを作って公開したところ先輩方にありがたい「ご指導」をたくさんいただいたので、身をもって学んだことを書こうと思います。

今回の話は同意の上でやってることなので、「勉強」等と称して他の人のサイトを攻撃するのはやめてください!

プレイグラウンドと化したアプリについて

本題から離れるので詳しくは書きませんが、WebSocketでリアルタイムで投稿内容が共有される掲示板のようなサイトです(チャットアプリに近いと思います)。
GitHub Repository

「ご指導」 と 対策

XSS (Cross Site Scripting)

攻撃者が作ったスクリプトを閲覧者のブラウザで実行させる行為(Cross Siteという名称は歴史的なものらしい・・分かりにくい)。

ご指導

最初、サーバーからクライアント全員にメッセージを共有する際、クライアントJSではテンプレート文字列を使ってHTMLを生成していました。
msg.message に投稿の内容が入って送られてくるとします。

document.getElementById("message-area").innerHTML += `<div>${msg.message}</div>`;

お分かりの通り、このコードではサーバーから送られてきた新規メッセージをそのままHTMLに突っ込んでしまっています。
このため、HTMLやJavaScriptとして意味を持つ文字列が送られてきてしまうと、ブラウザで任意のコードが実行できてしまいます。

例えば、<style> タグ等を入れられてしまうと急にWebページが回転しだしたり、サーバークライアント間の通信に使っているJSONで特別な意味を持つ " (ダブルクオーテーション)等が入るとサーバーでJSONがパースできなくなって落ちたりします。

対策

テンプレート文字列からではなく、JSの createElement() を使う。

const msgElm = document.createElement("div");
msgElm.textContent = msg.message;
document.getElementById("message-area").appendChild(msgElm);

textContent に入れることでブラウザがHTMLやJSなどの特別な文字列として処理しなくなるので、防ぐことができます。

また、送信時のJSONを作るときは

let data = `{msg: "${messageInput.value}"}`;

等のように文字列を直接作るのではなく、以下のように

let data = JSON.stringify({
  msg: messageInput.value
});

Objectを JSON.stringify()で変換する形をとることで、" 等がエスケープできる。

メッセージの長さ制限

ご指導

image.png
3000万を超える文字数が送られてきたら流石に重くなりますね・・・(実際にブラウザがカックカクになりました)。

対策

if (data.length > 1000) {
  throw 'too long';
}

これで完了かというとそうではない

さらにご指導を受けた

ブラウザからこういう感じで送られると先述のでは弾けないです。

socket.send(JSON.stringify({message: ["something".repeat(でかい数)]}));

つまり、こういうこと

{message: ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]}

ポイントは配列になっていることで、 length が配列長の 1 になってしまう!!

if (typeof message !== 'string') {
  throw 'not string';
}

なので、こんな感じで配列などの文字列以外のデータを弾くようにしないといけません。

まだご指導が続く

そもそもチェックするまでに落とされると困ります。
つまり、ここまでの方法はメモリを逼迫させるほどのリクエストが投げつけられることを想定していません(512MBしかメモリ積んでないサーバーに256MBのリクエストが飛んできて、そのままメモリに載ってしまうと一瞬で使い切ってプロセスが落とされてしまう、というか落とされた。)

const server = new wsServer({
  maxPayload: 10000
});

ここで maxPayload で制限することで、それを超えるリクエストが許可されなくなります。

運よく被害はなかったけど気を付けるべき「ご指導」

UTF-8(emojiなど)

今回、データベースへのメッセージの保存をしていないため(再起動すると履歴は消える)影響はなかったですが、emoji等(そもそも非ASCII文字だけでも)データベースが対応してない場合エラーとなってしまいます。なので、文字コードは前もって確認しておくべきです(今回ではないですが、以前それでエラーが出ました)。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした