joppot

コピペで絶対動く。説明を妥協しない

プログラミング

javascript + Reactでhtmlタグの削除と許可とXSS対策

投稿日:


概要

みなさんこんにちはcandleです。今回はReactで許可したhtmlタグだけを表示し、それ以外のタグは削除するプログラムを書いてみます。

注意、本来ユーザーなどが投稿した内容をそのまま表示させるのはXSSなどのセキュリティ上危険になる可能性があります。

また、私自身も1プログラマでセキュリティの専門家では無いのでコードにバグがあったりする可能性があります。もちろん、検証し、十分にテストはしていますが、もしもこの記事を閲覧している方でコードの脆弱性を発見された方がおりましたらコメント欄で指摘していただけると助かります。

前提

なし

サンプルの準備

もしも、すでにReactプロジェクトがある方はそちらを、無い方はサンプルプロジェクトを作って見てください

create-react-app htmltag_sample
cd htmltag_sample

準備が整いました。

実装

適当なコンポーネントファイルを開きます。サンプルコードを実践する方はsrc/App.jsを開きます。

最初に、ユーザーの投稿をテキストエリアから読み込み、表示するコードを記述します。

import React, { Component } from 'react'

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      content: '',
    }
    this.handleFormInputChanged = this.handleFormInputChanged.bind(this)
  }

  handleFormInputChanged(event) {
    this.setState({
      [event.target.name]: event.target.value,
    })
  }
  render() {
    return (
      <div style={{ textAlign: 'center' }}>
        <div>サンプル</div>
        <textarea
          name="content"
          value={this.state.content}
          onChange={this.handleFormInputChanged}
          style={{
            width: '400px',
            height: '300px',
            fontSize: '15px',
          }}
        />
        <div style={{ marginTop: '10px' }}>{this.state.content}</div>
      </div>
    )
  }
}

export default App

こうすると、テキストエリアで記述した内容が表示されます。

しかし、見た所、pタグがそのまま表示されています。これではタグ許可云々の前にタグが表示できないので、dangerouslySetInnerHTMLを使います。しかしこれはXSSの脆弱性を持っています。
コードを変更しましょう。

<div style={{ marginTop: '10px' }}>{this.state.content}</div>

をこうします。

<div dangerouslySetInnerHTML={{ __html: this.state.content }} />

これはこの記事によると<script>タグのXSSの対処はできています。

https://github.com/facebook/react/issues/8838

<a><iframe>のXSSは対応していません。

テキストエリアに試しに記述して見てください。

<script>alert("1");</script>

スクリプトタグは問題ないです。
aタグのXSSは機能してしまいます。

<a onmouseover="alert(1)">XSS</a>

iframeも同様に発動します。

<iframe src="javascript:alert('1');"></iframe>

指定したタグ以外削除

最初に指定したタグ以外を削除する関数を作ります。プログラムのベースはこちらの記事を参考にさせていただきました。これを、コンポーネントのどこかに書きましょう。

  strip_tags(input, allowed) {
    allowed = (((allowed || '') + '').toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join('')
    const tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi
    return input.replace(tags, ($0, $1) => (allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''))
  }

使い方はrender関数内で呼び出しそのままdangerouslySetInnerHTMLに投げます。

<div style={{ marginTop: '10px' }}>
  <div dangerouslySetInnerHTML={{ __html: this.strip_tags(this.state.content, '<a>') }} />
</div>

上の例ではaタグは許可してそれ以外は削除しています。

試しに動かして、こんな内容を書いて見ましょう。

<iframe src="javascript:alert('XSS');">hello</iframe>

<h1>H1 tag</h1>

<a href="https://www.google.com" target="_blank">google</a>

許可されたタグだけ表示されていますね。

しかし、これもいくつか問題があります。許可されたタグであればonEventやsytleが実行できてしまいます。そこで次にこういったtagのプロパティを削除する関数を作ります。

<a onmouseover="alert(document.cookie)">XSS</a>

タグのプロパティの削除と許可

上で見た通り、タグを許可するだけだと、悪意のあるユーザにonmouseover="alert(document.cookie)"style="font-size: 800px"などで攻撃される可能性があります。

この関数を追加します。

  strip_properties(input, allowed) {
    allowed = (((allowed || '') + '').toLowerCase().match(/[a-z][a-z0-9]*/g) || []).join('')
    const properties = /\s([a-z][a-z0-9]*)="[^"]*"/gi
    return input.replace(properties, ($0, $1) => (allowed.indexOf($1.toLowerCase()) > -1 ? $0 : ''))
  }

使うときはこのようにします。 aタグのみを許可しているのでhreftargetプロパティを許可しています。

<div style={{ marginTop: '10px' }}>
  <div
    dangerouslySetInnerHTML={{
      __html: this.strip_properties(this.strip_tags(this.state.content, '<a>'), ['href', 'target']),
    }}
  />
</div>

先ほどのXSSコードを試しても問題なさそうです。

<script>alert("1");</script>
<a onmouseover="alert(1)">XSS</a>
<iframe src="javascript:alert('1');"></iframe>

完成版サンプルコード

最後にサンプルコードを記述しておきます。

import React, { Component } from 'react'

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      content: '',
    }
    this.handleFormInputChanged = this.handleFormInputChanged.bind(this)
  }

  handleFormInputChanged(event) {
    this.setState({
      [event.target.name]: event.target.value,
    })
  }

  strip_properties(input, allowed) {
    allowed = (((allowed || '') + '').toLowerCase().match(/[a-z][a-z0-9]*/g) || []).join('')
    const properties = /\s([a-z][a-z0-9]*)="[^"]*"/gi
    return input.replace(properties, ($0, $1) => (allowed.indexOf($1.toLowerCase()) > -1 ? $0 : ''))
  }

  strip_tags(input, allowed) {
    allowed = (((allowed || '') + '').toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join('')
    const tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi
    return input.replace(tags, ($0, $1) => (allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''))
  }

  render() {
    return (
      <div style={{ textAlign: 'center' }}>
        <div>サンプル</div>
        <textarea
          name="content"
          value={this.state.content}
          onChange={this.handleFormInputChanged}
          style={{
            width: '400px',
            height: '300px',
            fontSize: '15px',
          }}
        />
        <div style={{ marginTop: '10px' }}>
          <div
            dangerouslySetInnerHTML={{
              __html: this.strip_properties(this.strip_tags(this.state.content, '<a>'), ['href', 'target']),
            }}
          />
        </div>
      </div>
    )
  }
}

export default App

まとめ

どうでしょうか?
もしも、問題等があればコメント欄で報告お願いします。

スポンサードリンク

「為になったなぁ」と思ったら、シェアお願いします。

-プログラミング
-,

執筆者:


comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

関連記事

ruby on railsのckeditorでcarrierwaveを使って画像をアップロードする

概要 みなさんこんにちはcandleです。今回はckeditorの画像のアップロードを行ってみたいと思います。 ckeditorの画像のアップロードは公式githubにも記載されているのですが、まあや …

React webでreact-simple-formatを使う

概要 みなさんこんにちはcandleです。今回はreactでsimple-formatを使ってみたいと思います。simple-formatは割とRuby on Railsでは有名で、よく使われます。R …

Rails pluginを使ったgemの作り方[1](基本設定編)

概要 みなさんこんにちはcandleです。今回からできればシリーズ物として、railsのgemを作っていこうと考えています。なぜかgemの作り方の詳しい解説サイトが日本語で少ないので未だgemを満足に …

railsのcoffee scriptをurl判定をして実行をする方法

概要 みなさんこんにちはcandleです。 今回はcoffeescriptを任意のURLで実行する様にする関数を作ります。 というのはrailsは最終的にassetspiplineでjavascrip …

memcachedサーバとruby on railsのdalliを使用してセッションを管理する

概要 みなさんこんにちはcandleです。今回はmemcachedサーバを利用してrailsのセッションを管理してみたいと思います。 一般にrailsでセッション管理をしていると、ブラウザのcooki …


ベンチャー企業のCTOをやってます。大学時代にプログラミングを始め、javaから入門し、C++へて、PHPへと進み、会社ではRailsを使用。自動化が大好きなプログラマー