セキュリティ

目次

CSRF保護

CSRF(Cross-Site Request Forgery:クロスサイトリクエストフォージェリ)は、ユーザーが意図しないリクエストを第三者が送信する攻撃手法です。攻撃者は、認証されたユーザーを利用してそのユーザーの権限で悪意ある操作を行わせることを狙います。

a-blog cms を利用すればCSRF攻撃からサイトを簡単に保護できます。

CSRF攻撃の防止

a-blog cms では、CSRFトークンを使用してCSRF攻撃に対応しています。

システムによって管理されているユーザーセッションごとにCSRF「トークン」を自動的に生成します。 このトークンはユーザーのセッションに保存され、CMSが生成するHTMLにも自動的に埋め込まれるようになっております。

ユーザーが何かリクエストする際には、テンプレートに埋め込まれたCSRFトークンも一緒に送信することで、ユーザーセッションに保存されているトークンと比較し、悪意あるリクエストを排除することが出来るようになります。

以下では、CSRFトークンが使用されるリクエストや、CSRFトークンの生成方法について解説します。

CSRFトークンが必要な操作

a-blog cms で CSRFトークンが必要な操作について列挙します。

POST操作

POSTモジュールは基本的にCSRFのチェックを行いますが、例外として以下のPOSTモジュールはCSRFトークンをチェックしません。

  • ACMS_POST_2GET
  • ACMS_POST_2GET_Ajax
  • ACMS_POST_Download
  • ACMS_POST_Login_Check
  • ACMS_POST_Member_SigninRedirect

Ajax(ポストインクルード)によるGETリクエスト

通常GETリクエストは、CSRFトークンをチェックしませんが、Ajaxリクエストの場合、CSRFトークンによる認証をする場合があります。

URLコンテキストで「tpl」が指定されている場合

通常以下のようなURLコンテキストにtplが入ったURLは404になり取得できません。

https://example.com/news/tpl/search.html

ただAjaxの場合は、CSRFトークンを使用して取得することが可能です。

取得HTMLが部分HTMLだった場合

通常a-blog cmsは、部分的なHTMLの取得を許可しませんが、Ajaxの場合は、CSRFトークンを使用することで部分的なHTML取得が可能になります。

CSRFトークンによるチェックをするには、private/config.system.yaml で「ajax_security_level」が「2」に設定されている必要があります。

ajax_security_level: 2 # ajaxリクエストのセキュリティレベルを設定します。(0: チェックなし 1: RefererとHttpヘッダーを確認 2: CSRFトークン確認)

CSRFトークンの生成条件

以下の条件の場合にユーザーセッションをスタートさせ、CSRFトークンを生成します。

  • ログインしている時
  • POSTリクエストの時
  • 「ACMS_POST_Form_」から始まる文字列がテンプレートにある時
  • 「ACMS_POST_Comment_」から始まる文字列がテンプレートにある時
  • 「ACMS_POST_Shop」から始まる文字列がテンプレートにある時
  • 「ACMS_POST_2GET_Ajax」から始まる文字列がテンプレートにある時
  • テンプレートに「check-csrf-token」文字列がある時
  • シークレットブログ・カテゴリーだった時
  • ログインページなど認証系の画面の時
  • phpで「IS_OTHER_LOGIN」定数が定義されている時

CSRFトークンを生成によりセッションスタートしてしまうことで、ブラウザキャッシュやCDNなどでキャッシュが利用できなくなってしまいます。必要な場合のみCSRFトークンを生成するようにしています。

CSRFトークンの埋め込み場所

生成されたCSRFトークンはレスポンスされるHTMLに埋め込まれます。

form要素

HTMLのform要素内の最後に、自動でinput要素が埋め込まれます。

<form method="post">
  ...
  <input type="hidden" name="formToken" value="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
</form>

metaタグ

HTMLのメタタグとして、head要素内に自動で埋め込まれます。

<meta name="csrf-token" content="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">

a-blog cms の基本機能を利用する場合、基本的には何も設定しなくてもCSRFトークンを含めた形でリクエストする形になっているため対応は不要です。

標準機能外でCSRFトークンを利用する

標準機能は、特に何もしなくてもCSRF対策がされていますが、独自にJavaScriptでリクエストを送ってレスポンスを受け取る場合などは、自身でCSRFトークンをリクエストに含める必要があります。

POSTリクエストの場合

POSTデータに以下情報を含める必要があります。

formToken: xxxxxxxxxxxxxxx

独自に開発したPOSTモジュールにJavaScriptからリクエストする例

const csrfToken = document.querySelector('meta[name="csrf-token"]').content; // メタタグからCSRFトークンを取得
const formData = new FormData();
formData.append('ACMS_POST_CustomModule', 'exec');
formData.append('formToken', csrfToken);

const response = await fetch(url, {
  method: 'POST',
  body: formData,
});

GETリクエストの場合

HTTPヘッダーに以下情報を必要があります。

Referer: https://example.com/xxxxxxx
X-Requested-With: XMLHttpRequest
X-Csrf-Token: xxxxxxxxxxxxxxxxxxx

HTMXライブラリ にCSRFトークンをセットする例

document.addEventListener("htmx:configRequest", function(event) {
  const csrfToken = document.querySelector('meta[name="csrf-token"]').content; // メタタグからCSRFトークンを取得
  event.detail.headers['X-CSRF-Token'] = csrfToken;
});

Ajax リクエストと tpl コンテキスト

Ver. 2.11.25 以降のバージョンでデフォルトで tplコンテキストの設定が無効になるような設定になりました。

理由:URLを変更すれば好きなテンプレートを簡単に指定することが出来ることにより、意図しないページが無制限にできてしまいため、セキュリティ・SEO対策としてURLコンテキストによるtpl指定は制限するようになりました。


ポストインクルードや htmx などの Ajax アクセスで取得するテンプレートは、基本テンプレートを指定してインクルードするので、上記の制限によりうまくインクルードされない可能性があります。 CMSのバージョンによって対応が変わりますので、以下ご確認ください。


Ver. 3.1.16 以下 & Ver. 2.11.25 以上の場合

例えば以下のようなTPL指定をするとURLは「https://example.com/xxxx.html/tpl/include/thumbnail.html」になり、URLでtpl指定をしてしまうことになります。

<input type="hidden" name="eid" value="%{EID}" />
<input type="hidden" name="tpl" value="include/thumbnail.html" />

このままでは制限をうけて正常にインクルードできないので、以下の方法により回避します。

設定を全体を解除する(非推奨)

private/cofig.system.yaml にある forbid_tpl_url_context を off に設定します。

forbid_tpl_url_context: off

この方法は、制限をなくしてしまう設定になるので、セキュリティ・SEO的に非推奨になります。古いバージョンからのアップデートで一時的にオフにしたいなど、理由がある場合のみ指定ください。


設定を部分的に解除する


private/cofig.system.yaml にある allow_tpl_path に tpl 指定で利用するファイルを指定します。複数ある場合にはカンマで区切って設定します。

allow_tpl_path: [inlcude/search.html, hoge/custom.html]

Ver. 3.1.17 以上の場合

Ver. 3.1.17で、特に意識・設定することなく、ポストインクルードや htmx など、Ajax アクセスでの tpl 指定を利用できるように改善されました。

改善内容

private/config.system.yaml の「ajax_security_level」の設定に応じて、Ajax のアクセスを認証することにより、直接のGETリクエストと、Ajax でのリクエストを区別することが出来るようになり、tpl指定の制限をしたまま、ポストインクルードや htmx などの Ajax リクエストは許可するということが出来るようになりました。

ajax_security_level: 2 # ajaxリクエストのセキュリティレベルを設定します。(0: チェックなし 1: RefererとHttpヘッダーを確認 2: CSRFトークン確認)

Ajaxセキュリティレベル

レベル0

ajax_security_level: 0

レベル0は、特になにも認証をしないので、基本的には設定しないでください。

レベル1

ajax_security_level: 1

レベル1は、RefererとHttpヘッダーを確認して、Ajaxリクエストであることを確認します。ポストインクルードや htmx を利用すれば自動でHTTPヘッダーは付与されるので、特に意識する必要はありません。

もしポストインクルードや htmx ではなく独自のAjaxリクエストで、HTMLを取得する場合は以下HTTPヘッダーを付与ください。

Referer: https://example.com/xxxxx/xxxx.html
X-Requested-With: XMLHttpRequest

レベル2

ajax_security_level: 2

レベル2は、レベル1のチェックに加え、CSRFトークンを使った認証を行います。ポストインクルードや htmx を利用すれば自動でHTTPヘッダーにCSRFトークンが付与されるので、特に意識する必要はありません。

もしポストインクルードではなく独自のAjaxリクエストで、HTMLを取得する場合は以下HTTPヘッダーを付与ください。

Referer: https://example.com/xxxxx/xxxx.html
X-Requested-With: XMLHttpRequest
X-Csrf-Token: xxxxxxxxxxxxxxxxxxx

X-Csrf-Token に設定する値は、テンプレートに「check-csrf-token」文字列を追加することにより、CMSで追加できます。例えばクラス名で指定するなどです。

<div class="check-csrf-token">
...
</div>

CSRFトークンは、metaタグに生成されています。JavaScriptなどでこの値を取得してご利用ください。

<meta name="csrf-token" content="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
const csrfToken = document.querySelector('meta[name="csrf-token"]').content

CSRFトークンを生成すると、セッションがスタートしてしまうので、ブラウザキャッシュなどが利用できなくなります。必要なページのみ指定するようにしてください。

HTMLPurifier による HTML サニタイズ


a-blog cms は、エントリー本文などユーザーが入力した HTML をテンプレート変数として出力する際、HTMLPurifier を使って XSS につながる要素・属性を取り除きます。このページでは、標準のサニタイズ処理と、設定・Hook による拡張方法を説明します。

テンプレートでの出力とサニタイズ

テンプレート変数は既定で エスケープされ、HTML タグは &lt; などの文字列として表示されます。エントリー本文のように HTML を HTML のまま出力したいときは raw 校正オプションを付けます。サニタイズ(HTMLPurifier)は、この raw 出力に対して危険な要素・属性だけを取り除き、安全な HTML を残す処理です(エスケープのように全体を文字化するのではなく、HTML の構造を保ったまま無害化します)。

標準テンプレートでのサニタイズの効き方は、校正オプションと config で決まります。

書き方

挙動

{vars}

エスケープして出力(既定・HTML はタグごと文字化され安全)

{vars}[raw]

HTML をそのまま出力。strip_dangerous_tagon のときは自動でサニタイズされる

{vars}[raw|allow_dangerous_tag]

strip_dangerous_tagon でもサニタイズせずに出力する。信頼できる HTML や Google Tag Manager などの script タグの出力が必要な場合にのみ利用します。

Twig テンプレートでは {{ entry.body|safe_html }} がサニタイズ付き出力、{{ entry.body|raw }} が非サニタイズ出力にあたります。

サニタイズが適用されるタイミング

サニタイズは HTML を保存するときではなく、出力するときに適用されます。データベースに保存されている HTML 自体が書き換わるわけではありません。代表的には次の場面です。

  • エントリー本文などのHTML文字列を raw でテンプレート出力するとき

  • ページキャッシュを再生成するとき

  • システム更新時にコンテンツを再生成するとき

出力のたびに適用されるため、dangerous_tags や iframe の許可ホストといった設定を変更すると、既存のエントリーもキャッシュの再生成タイミングで新しい方針に沿って出力し直されます。

自作のモジュールや拡張から同じサニタイズ処理を利用したい場合は、次のように呼び出せます。

$html = Acms\Services\Facades\Application::make('html-purifier')->clean($html);

サニタイズ設定

有効無効 (strip_dangerous_tag)

標準テンプレートでraw 校正オプション利用時の変数の出力時にデフォルトで HTMLPurifier を利用したサニタイズを適用するかどうかを設定します。

strip_dangerous_tag: on # on | off 変数から標準で危険なタグ(dangerous_tagsで指定)を削除するか指定

新規インストール時のデフォルトは on です。

禁止タグ(dangerous_tags)

private/config.system.default.yaml のデフォルト値は次のとおりです。

dangerous_tags: ['script', 'form']

ここに列挙したタグは丸ごと削除されます。<font> など HTML 標準で非推奨・廃止されたタグは、dangerous_tags の指定に関わらず常に削除されます。

iframedangerous_tags ではなく、次に説明する whitelist で制御します。

iframe のホスト whitelist (Ver. 3.2.25 ~)

html_purifier_iframe_allowed_hosts で、src を許可するホスト(パス prefix 可)を指定します。

html_purifier_iframe_allowed_hosts: [
    'www.youtube.com/embed|fullscreen; encrypted-media; picture-in-picture|',
    'www.youtube-nocookie.com/embed|fullscreen; encrypted-media; picture-in-picture|',
    'player.vimeo.com/video|fullscreen; picture-in-picture|',
    'www.google.com/maps/embed'
]

書式は host[/pathPrefix][|allow[|sandbox]] です。

要素

説明

host[/pathPrefix]

許可する src の前方一致。境界は / ? # または文字列終端(例: www.youtube.com/embed/embed/... のみ許可し /watch は拒否)

allow

セミコロン区切りの Permissions-Policy 機能トークン(例: fullscreen; encrypted-media

sandbox

スペース区切りの sandbox トークン

allow / sandbox は「編集者が要求できる capability の許可リスト」です。エントリー側の <iframe> に書かれた allow(セミコロン区切り)/ sandbox(スペース区切り)のうち、ここに列挙したトークンだけが残り、許可リスト外のトークンは削除されます。編集者が allow / sandbox を書いていなければ何も付与されません(強制で付与されることはありません)。ホスト部分だけを書いた場合は許可リストが空になり、編集者が何を書いても capability が付与されない最小権限になります。

  • 許可ホストにマッチしない <iframe> は、src だけが取り除かれて空の <iframe> として残ります(要素ごと削除されるわけではありません)。

  • html_purifier_iframe_allowed_hosts: [] にすると、すべての iframe の src が無効化されます。

  • 設定は private/config.system.yaml 側で上書き(追加・削除)できます。


HtmlPurifier によるサニタイズは HTML の構造・属性の許可リストを前提に構成されています。iframe whitelist は src を縛るものであり、フレーム表示の最終的な可否はHTTPヘッダー側の X-Frame-Options / CSP frame-ancestors にも依存します。多層防御の観点では、CSP の frame-src も別途あわせて運用してください。


Hook による拡張

dangerous_tags や whitelist だけで表現できない要件(独自タグ・属性の許可、URI スキームの追加など)は、extendsHtmlPurifierConfig / extendsHtmlPurifierDefinition の 2 つのフックで拡張します。サイト拡張(extension/acms/Hook.php)やプラグインのフッククラスに実装します。

2 つのフックの違い(Ver. 3.2.25 ~)

フック

発火タイミング

用途

extendsHtmlPurifierConfig(HTMLPurifier_Config $config)

カスタム HTML 定義を取得する

HTML.* / URI.* / CSS.* / Attr.* の config を上書き・追加する

extendsHtmlPurifierDefinition(HTMLPurifier_HTMLDefinition $def, HTMLPurifier_Config $config)

カスタム HTML 定義を取得した

addElement / addAttribute で許可する要素・属性を追加する

HTML.* 系の設定は定義の取得前に確定させる必要があるため、許可タグ・属性・URI スキームの調整は必ず extendsHtmlPurifierConfig 側で行ってください。定義取得後にしか行えない要素・属性の追加(addElement / addAttribute)は extendsHtmlPurifierDefinition 側で行います。

/**
 * HTMLPurifier の設定構築時に呼ばれる拡張ポイント(カスタム HTML 定義の取得前)
 */
public function extendsHtmlPurifierConfig(\HTMLPurifier_Config $config): void
{
    // 例: data: スキームの画像を許可する
    $config->set('URI.AllowedSchemes', ['http' => true, 'https' => true, 'mailto' => true, 'data' => true]);

    // 例: 特定タグをさらに禁止する(既定値を上書き)
    $config->set('HTML.ForbiddenElements', ['script', 'iframe', 'form', 'object']);
}

/**
 * HTMLPurifier のカスタム HTML 定義の構築時に呼ばれる拡張ポイント(定義の取得後)
 */
public function extendsHtmlPurifierDefinition(\HTMLPurifier_HTMLDefinition $def, \HTMLPurifier_Config $config): void
{
    // 例: <details> / <summary> を許可し、独自 data 属性を通す
    $def->addElement('details', 'Block', 'Flow', 'Common', ['open' => 'Bool#open']);
    $def->addElement('summary', 'Inline', 'Inline', 'Common');
    $def->addAttribute('div', 'data-my-widget', 'CDATA');
}

雛形(ablogcms/extension/acms/Hook.php)にも同じ形の空実装とサンプルコメントが用意されています。


キャッシュ

カスタム HTML 定義はキャッシュされます。iframe whitelist(html_purifier_iframe_allowed_hosts)を config で変更した場合は自動でキャッシュが再生成されますが、extendsHtmlPurifierConfig / extendsHtmlPurifierDefinition フックで追加・変更した内容は自動では反映されません。

フックの実装を追加・変更したときは、必ず管理画面からキャッシュを削除してください。 削除すると html-purifier 用のキャッシュディレクトリが消え、次回アクセス時に新しい定義でキャッシュが再生成されます。挙動が反映されていないように見えたときも、まずキャッシュ削除を試してください。


注意点

  • extendsHtmlPurifierConfig / extendsHtmlPurifierDefinition はフック機能が有効(HOOK_ENABLE)であることが前提です。フックを無効化している環境では呼ばれません。

  • 独自属性を許可する際は、フリーフォームの CDATA を安易に増やさず、可能な限り EnumBool など型付きの AttrDef を使ってください。