PR
PR

【Python_study_Day12】html変換サービスのセキュリティ対策を学ぶ

記事内に広告が含まれています。

Python学習12日目です!

前回はHTML変換サービスをGunicorn + Nginxの環境で動かすところまでできました。

テスト環境から一歩進み、本番環境に近い形でサービスを動かせるようになったので、ここからはセキュリティ対策について学んでいきます。

Webサービスのセキュリティ対策を教えてもらう

正直なところ、Webサービスのセキュリティについてはほとんど知識がありません。

ユーザーからの入力を受け付けるサービスを公開するとなると、前回までのような単なる時計サービスとは違い、様々な危険があるはずです。

そこで、今回も頼りになるGemini先生に現在のapp.pyを見てもらい、どのような対策が必要かセキュリティの基礎から教えてもらうことにします。

セキュリティ対策前のapp.py

# 必要なモジュールをインポート
import html
from flask import Flask, render_template, request

# Flaskアプリケーションのインスタンスを作成
app = Flask(__name__)

# '/' (ルートURL) にアクセスがあった場合の処理を定義
@app.route('/', methods=['GET', 'POST'])
def index():
    escaped_html = None
    if request.method == 'POST':
        original_html = request.form['html_code']
        escaped_html = html.escape(original_html)
    return render_template('index.html', escaped_html=escaped_html)

# このファイルが直接実行された場合に開発用サーバーを起動
#if __name__ == '__main__':
#    app.run(debug=True)

教えてもらったセキュリティ対策

Gemini先生にapp.pyを見てもらい教えてもらったセキュリティ対策の内容を元に、Webサービスをインターネットに公開し、ユーザーからの入力を受け付ける際に特に重要な6つのセキュリティ対策について解説します。

  1. デバッグモードの無効化
  2. 開発中に便利なFlaskのデバッグモードは、本番環境では絶対に無効にしなければなりません。

    Flask開発サーバのデバッグモードを有効化した状態でWebサービスを公開すると、エラー発生時にサーバー内部の情報(ソースコード、環境変数、ファイルパスなど)がブラウザに表示されてしまいます。

    悪意のあるユーザーはこれを利用して、サーバー上で任意のコードを実行することも可能になるため、サーバーが完全に乗っ取られる深刻なリスクがあります。

    本番環境に移行する際は、必ずデバッグモードをオフにしましょう。

  3. 本番環境用のサーバー構成
  4. Flaskに組み込まれている開発用サーバーは、多数の同時アクセスを想定していません。

    そのため、本番環境ではパフォーマンスと安定性を確保するために、専用のサーバー構成が必要です。

    一般的には、GunicornやuWSGIといったWSGIサーバーでFlaskアプリケーションを動かし、その前にNginxなどのWebサーバーを配置します。

    この構成にすることで、大量のリクエストを効率的に処理し、サービスの安定稼働を実現できます。

  5. SSL/TLS(HTTPS)の利用
  6. HTTP通信は暗号化されていないため、ユーザーが送受信するデータがネットワーク上で第三者に盗聴・改ざんされる可能性があります。

    HTTPS通信はデータを暗号化し、データの機密性と完全性を保護します。

    HTTPS(SSL/TLS)を利用することで、通信内容が暗号化され、データの機密性と完全性が保護されます。

    Let's Encryptなどのサービスを使えば、無料でSSL証明書を取得できるので、ユーザーが安心してサービスを利用できるように、HTTPS化は必ず行いましょう。

  7. 入力検証 (Input Validation) を徹底する
  8. ユーザーからの入力は、決して信用してはいけません。

    不適切な入力は、アプリケーションのクラッシュやセキュリティ脆弱性(XSSなど)、データの破損につながります。

    • サーバーサイドでの検証を必須にする
    • クライアントサイド(ブラウザ側)の検証は簡単に回避されるため、必ずサーバー側で厳密な検証を行います。

    • データの形式と範囲をチェックする
    • 入力のタイプ(数値、文字列など)、文字数、許可される文字の種類などを厳しくチェックします。

    • 特殊文字を適切にエスケープ・サニタイズする
    • ユーザー入力をHTMLとして表示する際は、html.escape()を使って特殊文字を安全な形式に変換します。

  9. CSRF (Cross-Site Request Forgery) 対策を導入する
  10. CSRF攻撃は、攻撃者がユーザーをだまして、意図しないリクエストを送信させるものです。

    例えば、ユーザーがログインしている状態で悪意のあるサイトを閲覧すると、そのサイトから勝手にWebサービスに「HTML変換リクエスト」が送信され、不要なリソース消費や、場合によってはより悪質な操作が行われる可能性があります。

    これを防ぐために、Flask-WTFのような拡張機能を使って、CSRFトークンをフォームに組み込む対策を導入します。

  11. レートリミット
  12. 意図的なDDoS攻撃や、誤ったプログラムによるアクセス集中からサーバーを守るために、レートリミット(アクセス制限)を設定します。

    短時間に大量のリクエストが送られてきた場合、サーバーに負荷がかかり、サービスが停止してしまう可能性があります。

    NginxやFlask-Limiterといったツールを使えば、設定した回数以上のアクセスを制限し、サーバーの負荷を軽減できます。

今回の学習テーマ

教えてもらった上記6つの対策のうち、1~3番(デバッグモード無効化、本番環境サーバー構成、HTTPS化)は、すでに行う方法を知っているので、今回はそれ以外の「4.入力検証 (Input Validation) を徹底する」について詳しく勉強することにします。

入力検証 (Input Validation) のやり方を学ぶ

ユーザーからの入力を安全に受け付けるためには、入力検証(Input Validation)が不可欠です。

フォームの定義とユーザからの入力を検証するためのライブラリとして、WTFormsとFlask-WTFを使用する方法をおすすめされました。

  • WTForms: フォームの定義と検証を行うための本体ライブラリです。
  • Flask-WTF: FlaskとWTFormsを統合するための拡張機能です。CSRF保護などを提供します。

WTFormsを使うメリット

WTFormsを導入することの主なメリットは以下の通りです。

  • 多様な検証ルール追加が簡単設定可能
  • 「必須入力」「最小/最大文字数」「メールアドレス形式」「数字のみ」「正規表現に合致するか」など、さまざまな検証ルールを簡単に設定できます。

  • エラーメッセージを自動生成
  • 検証に失敗した場合、各フィールドに対して分かりやすいエラーメッセージを自動で生成し、テンプレートに渡してくれます。

    手動でエラー処理を書く手間が省け、コードがシンプルになります。

  • CSRF (Cross-Site Request Forgery) 対策も自動化
  • WTFormsとFlask-WTFを組み合わせることで、CSRF攻撃フォームを保護するためのCSRFトークンを自動で生成・検証してくれます。

    自力で実装するよりも、はるかに安全で確実です。

WTFormsとFlask-WTFのインストール

uvを使ってWTFormsとFlask-WTFをインストールします。

インストールする際には、Pythonの仮想環境を有効化しておきます。

(html_converter) $ uv pip install Flask-WTF WTForms
Resolved 9 packages in 347ms
Prepared 2 packages in 100ms
Installed 2 packages in 9ms
 + flask-wtf==1.2.2
 + wtforms==3.2.1

セキュリティ対策後のapp.pyとindex.html

いよいよ実際に入力検証とCSFR対策を行ったコードをGemini先生に用意してもらいます。

Gemini先生が提案してくれた、入力検証とCSRF対策の機能を追加したapp.pyとindex.htmlが、以下になります。

セキュリティ対策追加後のapp.py

# 必要なモジュールをインポート
import html
from flask import Flask, render_template, request
from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length


# Flaskアプリケーションのインスタンスを作成
app = Flask(__name__)

# CSRF保護のためにSECRET_KEYが必要になる
app.config['SECRET_KEY'] = 'your_secret_key'

# HTMLエスケープフォームの定義
class HtmlEscapeForm(FlaskForm):
    html_code = TextAreaField(
        '変換したいHTMLコードを入力してください:', 
        validators=[
            DataRequired(message="HTMLコードは必須です。"), 
            Length(max=10000, message="入力されたHTMLコードが長すぎます(最大10,000文字)。") 
        ]
    )
    submit = SubmitField('変換')

# '/' (ルートURL) にアクセスがあった場合の処理を定義
@app.route('/', methods=['GET', 'POST'])
def index():
    form = HtmlEscapeForm()
    escaped_html = None

    if form.validate_on_submit():
        original_html_input = form.html_code.data

        decoded_html = html.unescape(original_html_input)
        escaped_html = html.escape(decoded_html)

    return render_template('index.html', form=form, escaped_html=escaped_html)

# このファイルが直接実行された場合に開発用サーバーを起動
if __name__ == '__main__':
   app.run(debug=True)

セキュリティ対策追加後のtemplates/index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMLエスケープツール</title>
    <style>
        body { font-family: sans-serif; margin: 20px; }
        textarea { width: 80%; height: 200px; margin-bottom: 10px; }
        pre { background-color: #eee; padding: 10px; border: 1px solid #ddd; overflow-x: auto; }
        h2 { margin-top: 30px; }
        .error {
            color: red;
            font-size: 0.9em;
            margin-top: 5px;
            list-style: none; /* リストの点を消す */
            padding-left: 0; /* リストの余白を消す */
        }
        .form-group {
            margin-bottom: 15px;
        }
    </style>
</head>
<body>
<h1>HTML特殊文字変換ツール</h1>
<form method="POST">
    {{ form.csrf_token }}
        <div class="form-group">
            {{ form.html_code.label }}<br>
            {{ form.html_code(rows=10, cols=80, placeholder="例: <div>Hello!</div>") }}
            {% if form.html_code.errors %}
                <ul class="errors">
                {% for error in form.html_code.errors %}
                    <li class="error">{{ error }}</li>
                {% endfor %}
                </ul>
            {% endif %}
        </div>
        <div class="form-group">
            {{ form.submit() }}
        </div>
</form>

{% if escaped_html %}
    <h2>変換結果:</h2>
    <pre>{{ escaped_html }}</pre>
{% endif %}
</body>
</html>

app.pyのコードを詳しく解説

app.pyを1行づつGemini先生に解説してもらいながら、WTFormsとFlask-WTFを使った入力検証とフォーム定義の部分を詳しく学んでいきます。

必要なモジュールのインポート

WTFormsとFlask-WTFを使用するために、以下のモジュールをインポートを行います。

from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length

from flask_wtf import FlaskForm

Flask-WTFライブラリから、フォームの基本設計図となるFlaskFormのclass(クラス)を使用できるようにインポートしています。

from wtforms import TextAreaField, SubmitField

WTFormsライブラリから、Webフォームを構成する2種類のフィールドのclass(クラス)を使用できるようにインポートしています。

  • TextAreaField: 複数行のテキストを入力するための大きなテキストボックス(HTMLの<textarea>)を作成します。
  • SubmitField: フォームを送信するためのボタン(HTMLの<input type="submit">)を作成します。

from wtforms.validators import DataRequired, Length

WTFormsライブラリのvalidatorsモジュールから、フォームの入力値を検証するための2つのバリデーターのclass(クラス)を使用できるようにインポートしています。

バリデーターの解説

バリデーターとは、フォームに入力されたデータが正しいルールに従っているかをチェックするためのルールです。

  • DataRequired: このフィールドが入力必須であることを示すルールで、空のまま送信されることを防ぎます。
  • Length: 入力された文字数が指定した範囲内に収まっているかをチェックするルールです。

フォームの定義HtmlEscapeForm

Webページに表示するフォームの構造とルールをPythonのクラスとして定義しています。

# HTMLエスケープフォームの定義
class HtmlEscapeForm(FlaskForm):
    html_code = TextAreaField(
        '変換したいHTMLコードを入力してください:', 
        validators=[
            DataRequired(message="HTMLコードは必須です。"), 
            Length(max=10000, message="入力されたHTMLコードが長すぎます(最大10,000文字)。") 
        ]
    )
    submit = SubmitField('変換')

class HtmlEscapeForm(FlaskForm):

Webフォームの設計図となる新しいHtmlEscapeFormという名前のclass(クラス)を定義しています。

(FlaskForm)

FlaskFormは、Flask-WTFライブラリが提供している、FlaskFormのclass(クラス)を継承することを示しています。

これは「FlaskFormが持っている便利な機能をすべて受け継いだ、HtmlEscapeFormという新しいclassを作ります」という意味になります。

html_code = TextAreaField(...)

変数html_codeにWTFormsライブラリのTextAreaFieldを使って定義したフィールドの内容を格納しています。

'変換したいHTMLコードを入力してください:'

TextAreaFieldの最初の引数に指定された文字列「'変換したいHTMLコードを入力してください:'」は、そのテキストエリアが何を入力するためのものなのかをユーザーに説明するラベルとして機能します。

HTMLテンプレートで {{ form.html_code.label }}のように記述すると、この文字列がlabelタグとしてWebページに表示されます。

validators=[...]

入力内容を検証するためのルールを「,」区切のリスト形式で指定しています。

  • DataRequired(message="HTMLコードは必須です。")
  • DataRequiredはデータの入力が必須であることを意味し、messageの部分でユーザーが何も入力せずに送信した場合に表示されるエラーメッセージを指定しています。

  • Length(max=10000, message="入力されたHTMLコードが長すぎます(最大10,000文字)。")
  • 入力できる文字数を制限するルールを設定しています。

    • max=10000
    • 最大文字数を設定しています。

    • message="入力されたHTMLコードが長すぎます(最大10,00文字)。"
    • ユーザーの入力がこの文字数を超えてしまった場合に表示されるカスタムエラーメッセージです。

submit = SubmitField('変換')

変数submitに、WTFormsライブラリのSubmitFieldを使って定義した、送信ボタンのフィールド内容を格納しています。

引数として渡している '変換' という文字列が、ブラウザ上でボタンのラベルとして表示されます。

Webページからのフォーム送信を処理するFlaskの関数の定義

このindex()関数は、ウェブページからのフォーム送信をすべて受け付けて処理する関数になります。

def index():
    form = HtmlEscapeForm()
    escaped_html = None

    if form.validate_on_submit():
        original_html_input = form.html_code.data

        decoded_html = html.unescape(original_html_input)
        escaped_html = html.escape(decoded_html)

    return render_template('index.html', form=form, escaped_html=escaped_html)

form = HtmlEscapeForm()

class HtmlEscapeFormをインスタンス化して、変数fromに代入しています。

このform変数を使うことで、テンプレートにフォームを表示したり、送信されたデータを受け取り、検証、処理したりできるようになります。

インスタンス化とは

クラスを元に、実際に使えるオブジェクトを作成する作業です。

escaped_html = None

変数escaped_htmlにNoneを代入して初期化しています。

escaped_htmlは、変換後のHTML文字列を格納するための変数です。

最初にNone(「値が何もない」という意味)を代入しておくことで、フォームがまだ送信されていない場合でも、この変数が存在することを保証します。

if form.validate_on_submit():

form.validate_on_submit()は、フォームが送信され、かつ入力内容が正しかった場合にのみ処理を実行するための、Flask-WTFが提供する機能です。

このメソッドは、内部で以下の2つのチェックを一度に行います。

  • フォームが送信(POST)されたか?
  • ブラウザからのリクエストが、ページの表示(GET)ではなく、フォームの送信(POST)であるかを確認します。

  • 入力内容は正しいか?
  • フォームの各フィールドに設定されたバリデーター(例: DataRequired, Length)をすべて実行し、ルール違反がないかをチェックします。

ifブロック内のコードは、これら両方の条件を満たした場合にのみ実行されます。

このif文のおかげで、コードを簡潔に保ちつつ、不正なデータが処理されるのを防ぐことができます。

original_html_input = form.html_code.data

バリデーション(検証)を通過した、安全なデータをフォームのhtml_codeフィールドから取り出し、original_html_input変数に代入しています。

decoded_html = html.unescape(original_html_input)

ユーザーが入力したデータに、&lt;p&gt;のような文字が含まれていた場合、それを一度<p>のような元のHTMLタグに戻して、変数decoded_htmlに代入しています。

これは、二重にエスケープされるのを防ぐための処理です。

html.unescape

Webフォームやデータソースから得た文字列には、&lt;p&gt;のように、既にエスケープされた状態で保存されていることがあります。

html.unescape()は、このエスケープされた文字列を、<p>ような元のHTMLタグに戻す処理を行います。

これは、二重にエスケープされるのを防ぐための重要な処理となります。

html.unescapeはPythonの標準ライブラリであるhtmlモジュールに含まれているため、import htmlと書くだけで追加のインストールなしに利用できます。

escaped_html = html.escape(decoded_html)

変数decoded_htmlに格納されている文字列の、<p>のようなHTMLとして意味を持つ特殊文字を、&lt;p&gt;のような安全な文字列にエスケープし、変数escaped_htmlに代入しています。

html.escapeはPythonの標準ライブラリであるhtmlモジュールに含まれているため、import htmlと書くだけで追加のインストールなしに利用できます。

return render_template('index.html', form=form, escaped_html=escaped_html)

Pythonの処理結果をウェブブラウザに返しています。

render_template('index.html', ...

render_templateはFlaskが提供する関数で、templatesフォルダにあるindex.htmlというファイルを読み込み、HTMLページを生成(レンダリング)します。

... form=form, escaped_html=escaped_html)

render_templateに渡すキーワード引数で、Pythonの変数をHTMLテンプレートに「引き渡す」役割を担います。

  • form=form
    • 左辺のform: HTMLテンプレート内で使う変数名です。
    • 右辺のform: Pythonコードで作成したフォームオブジェクトです。

    これにより、HTML側で{{ form.html_code }}のようにしてフォームを呼び出せるようになります。

  • escaped_html=escaped_html
  • HTML側でescaped_htmlという変数名で、Pythonで処理した変換後の文字列を使えるようにしています。

本日のまとめ:WTFormsでフォームを安全に

今回は、HTML変換サービスにWTFormsとFlask-WTFを導入しフォーム入力の検証方法について学びました。

この2つのライブラリを使うことで、自分で複雑な検証ロジックを書くことなく、入力必須チェックや文字数制限といったルールを簡単に実装できることを体験しました。

また、html.unescape()とhtml.escape()を組み合わせることで、二重エンティティ化を防ぐ堅牢な処理も実現できました。

今回学んだのはWTFormsのごく一部の機能ですが、その便利さと重要性を強く実感できたので、より詳しい使い方について今後公式ドキュメントで深く学んでいくことにします。

次回の予定:CSRF対策に挑む!

次回は、本日学んだWTFormsとFlask-WTFが自動で提供してくれるCSRF対策について、その仕組みと重要性を学んでいく予定です。

セキュリティ対策の知識をさらに深めて、より安全なWebサービスへと進化させていくぞ!

コメント

タイトルとURLをコピーしました