2020-05-01

【Vue.js】メッセージ送信フォームの連投(イタズラ)防止機能を考える

連続投稿を防止する機能を実装した。オマケで連続投稿を防止する機能を無効にして連続投稿してイタズラする。

Article Image

はじめに

この記事は前回の記事「【Discord Bot】Vue.jsでDiscordへメッセージを送る機能を実装する」の続編である。

Vue.jsからDiscordへメッセージを送る場合の簡単なコードは上のページで行っているので参考にしてほしい。

本日の概要

投稿するテキストエリアを増やし、ダイアログも幾つか実装した。

特別難しいコードではないのでこのあたりの解説は省略。

本日はこのVue.js上で連投防止機能を考えたので解説する。

残念ながらそれでもイタズラは出来るので抜け穴を合わせて紹介する。

※全コードは下部にすべて貼り付けてある。解説は重要なところに絞って行う。

フォーム

次のようなフォームを作った。

2020 05 01 21h54 03

連投する

次のようなイタズラを入力する

2020 05 01 21h56 47

送信する。

2020 05 01 21h58 19

2020 05 01 21h58 40

送信できた。

私のDiscordに次のメッセージが送られてくる。

2020 05 01 21h59 14

即座にもう一度メッセージを送信してみる。

2020 05 01 22h00 00

ブロックされた。

原理

非常に簡単。

簡単すぎて驚くと思う。

投稿が完了した時に日付をローカルに保存

ブラウザの機能でローカルストレージというものがある。

これはクッキーとは違う。次のサイトが参考になる。

クッキーはもう古い!?HTML5 LocalStorageの使い方

これはVue.jsからも利用できる。コードは簡単で書き込みが成功した時に次のコードを実行する。

localStorage.date = new Date(); //イタズラ防止用:投稿時間を保存

これが簡単すぎて誤った使い方をする人がいるそうなので書いておくと

本当にnpm installもいらず、宣言もいらない。いきなりlocalStorageのメンバ変数に書き込むだけだ。

投稿するときに前回の投稿日時との差を確認

  //連投制限 連続投稿には1分待つ必要がある
  if (localStorage.date) {
    const postSpan = new Date(
      new Date() - new Date(localStorage.date)
    ).getMinutes();
    if (postSpan < 1) {
      this.dialogs.error_msg =
        "連続で投稿できません。しばらくお待ち下さい。";
      this.dialogs.error = true;
      return;
    }
  }

上のようなコードで1分以上経過しないと連続投稿できないようにした。

悪用してイタズラする

さて、このコードの抜け穴を使ってイタズラしてやろう。

残念だがVue.jsはクライアントサイドのプログラムなのでクライアント内部のデータを弄ってやればこの機能はスルーできてしまうのだ。

Chrome版

Chrome以外わからないのでこれで説明する。

まずF12を押す

2020 05 01 22h12 36

画面が小さい場合右上に「>>」が出ているので押す。「Application」を選択する。

2020 05 01 22h14 50

①が選択されたら②の「Local Storage」とあるところの小さな三角を押して③のサイトURLをクリックする。

ここではローカルサーバーを稼働しているので画像のようになっているが、私のサイトであればultra-noob.comとなっているはずだ。

最後に④がVue.jsによって書き込まれた変数である。Vue.jsはこの値を読み込んでイタズラかどうかを判断しているので

2020 05 01 22h18 06

右クリックでDELETEしよう。

こうすれば比較する日付がないため何度でも投稿できる。

2020 05 01 21h58 40

クライアントサイドなので仕方がない

できればイタズラはやめていただきたい。 サーバー側でフィルター処理ができる環境であればこのような処理は必要ないだろうがVue.jsはあくまでもクライアントサイドのプログラムであるためこのような抜け穴は仕方がない。

前回の書き込み日時がローカルストレージに保存されてない場合はページ読み込みから1分待たないと書き込めないなどの処理はできるだろうが、それでも抜け穴はいくらでもある。

もしかしたら良い防止策があるかもしれないが、この機能はおまけ程度だと考えれば良い。

所感

ローカルストレージはいろいろなサイトで使われている。この手法を覚えておけばデバッグ等で役立つだろう。 覚えておいて損はないと思う。

おまけ:全コード紹介

あまり美しいコードではないかもしれない。

Webhook URLは削除したので参考にそのまま記載してある。イタズラはやめていただきたい。

<template>
  <v-container>
    <!-- メッセージフォーム -->
    <v-card class="mx-auto" max-width="800">
      <v-card-title>メッセージ送信フォーム (Contact Form)</v-card-title>
      <v-card-text>
        <v-form ref="messageForm">
          <v-text-field v-model="formData.name" label="お名前 (Name)"></v-text-field>
          <v-text-field v-model="formData.replyTo" label="返信先 (E-mail, Twitter, etc)"></v-text-field>
          <v-textarea v-model="formData.message" :rules="[required]" label="[必須] メッセージ (Message)"></v-textarea>
        </v-form>
      </v-card-text>
      <v-card-actions>
        <v-spacer></v-spacer>
        <v-btn @click="check">送信(Submit)</v-btn>
      </v-card-actions>
    </v-card>

    <!-- 確認ダイアログ -->
    <v-dialog v-model="dialogs.check" max-width="300">
      <v-progress-linear :active="dialogs.loader" indeterminate color="blue" class="mb-0"></v-progress-linear>
      <v-card>
        <v-card-title>確認(Confirmation)</v-card-title>
        <v-card-text>
          <v-card-subtitle>この内容でよろしいですか?</v-card-subtitle>
          <p>[Name] {{formData.name}}</p>
          <p>[Reply-To] {{formData.replyTo}}</p>
          <p>[Message]</p>
          <p>{{formData.message}}</p>
        </v-card-text>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn @click="submit">OK</v-btn>
          <v-btn @click="dialogs.check=!dialogs.check">Cancel</v-btn>
        </v-card-actions>
      </v-card>
      <v-progress-linear :active="dialogs.loader" indeterminate color="blue" class="mb-0"></v-progress-linear>
    </v-dialog>

    <!-- エラーダイアログ -->
    <v-dialog v-model="dialogs.error" max-width="300">
      <v-progress-linear :active="dialogs.loader" indeterminate color="blue" class="mb-0"></v-progress-linear>
      <v-card>
        <v-card-title>
          <v-icon color="red">mdi-alert-circle-outline</v-icon>Error!
        </v-card-title>
        <v-card-text>
          <p>[エラー内容]</p>
          <p>{{dialogs.error_msg}}</p>
        </v-card-text>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn @click="dialogs.error=!dialogs.error">OK</v-btn>
        </v-card-actions>
      </v-card>
      <v-progress-linear :active="dialogs.loader" indeterminate color="blue" class="mb-0"></v-progress-linear>
    </v-dialog>

    <!-- 送信成功ダイアログ -->
    <v-dialog v-model="dialogs.success" max-width="300">
      <v-card>
        <v-card-title>
          <v-icon color="green">mdi-check-bold</v-icon>送信成功!
        </v-card-title>
        <v-card-text>
          送信されました。
          <br />ありがとうございました。
        </v-card-text>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn @click="dialogs.success=!dialogs.success">OK</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </v-container>
</template>

<script>
import axios from "axios";

export default {
  name: "PostForm",
  data: () => ({
    //ダイアログ関係のオブジェクト
    dialogs: {
      success: false,
      check: false,
      error: false,
      loader: false,
      error_msg: ""
    },
    //Webhook接続先URL
    webhook_url:
      "https://discordapp.com/api/webhooks/705382349235421205/k6seL_OdWEHzQb06WsST1CqVtxRXS51ziZZhPL2d2aktoC6lYvw9Kp7ZTNwdcHJAAMLY",
    //必須項目チェックのバリデータ
    required: value =>
      !!value || "メッセージは必須項目です。 Message is required.",
    //フォーム内のデータ
    formData: {
      name: "",
      replyTo: "",
      message: ""
    }
  }),
  mounted:()=>{
    document.title = "メッセージ送信フォーム - ultra-noob.com";
  },
  methods: {
    closeAllDialogs() {
      //ダイアログをすべてクローズ
      this.dialogs.success = false;
      this.dialogs.check = false;
      this.dialogs.error = false;
    },
    check() {
      //必須項目が入力されていれば確認用ダイアログを表示
      if (this.$refs.messageForm.validate()) {
        this.dialogs.check = true;
      }
    },
    submit() {
      this.dialogs.loader = true;
      //データの内容はcontent
      //\r で改行を入れることが出来る
      const data = {
        username: this.formData.name,
        content: `[Reply-to] ${this.formData.replyTo}\r[Message]\r${this.formData.message}`
      };
      //連投制限 連続投稿には1分待つ必要がある
      if (localStorage.date) {
        const postSpan = new Date(
          new Date() - new Date(localStorage.date)
        ).getMinutes();
        if (postSpan < 1) {
          this.dialogs.error_msg =
            "連続で投稿できません。しばらくお待ち下さい。";
          this.dialogs.error = true;
          return;
        }
      }
      //POSTするロジック
      axios
        .post(this.webhook_url, data)
        .then(() => {
          //投稿に成功したとき
          this.closeAllDialogs();
          this.dialogs.success = true;
          this.dialogs.loader = false;
          localStorage.date = new Date(); //イタズラ防止用:投稿時間を保存
        })
        .catch(err => {
          //通信でエラーがあったとき
          this.closeAllDialogs();
          this.dialogs.error = true;
          this.dialogs.error_msg = err;
          this.dialogs.loader = false;
        });
    }
  }
};
</script>


この記事のタグ

この記事をシェア


謎の技術研究部 (謎技研)