DataTablesでAPIやDBの値を一覧表示するとき、外部データやユーザー入力をHTMLとして扱うとXSSの入口になります。
この記事では、DataTablesで値を安全に表示するための基本形を、DataTable.render.text()とcolumns.renderを中心に整理します。
公式ページ
XSSとは
XSSは、悪意あるHTMLやJavaScriptがページ上で実行されてしまう脆弱性です。
たとえば、ユーザー名や商品名などの項目に以下のような文字列が登録されていたとします。
<img src=x onerror="alert('XSS')">
この値をHTMLとしてそのまま表示してしまうと、ブラウザ上でJavaScriptが実行される可能性があります。
DataTables自体を使っているかどうかに関係なく、外部から来た値をHTMLとして扱う場合は注意が必要です。
危険な例
以下は、DataTablesでHTML文字列をそのまま返している例です。
const users = [
{
name: '<img src=x onerror="alert(\'XSS\')">',
role: 'admin'
}
];
new DataTable('#usersTable', {
data: users,
columns: [
{
data: 'name',
title: '名前',
render: function (data) {
return data;
}
},
{ data: 'role', title: '権限' }
]
});
renderでdataをそのまま返すと、値がHTMLとして解釈される場合があります。
ユーザー入力や外部APIの値を扱う列では、この書き方は避けた方が安全です。
DataTable.render.text()を使う
DataTablesには、HTMLをエスケープしてテキストとして表示するための組み込みレンダラがあります。
new DataTable('#usersTable', {
data: users,
columns: [
{
data: 'name',
title: '名前',
render: DataTable.render.text()
},
{
data: 'role',
title: '権限',
render: DataTable.render.text()
}
]
});
DataTable.render.text()を使うと、<や>などがHTMLとして実行されず、文字列として表示されます。
外部データやユーザー入力をそのまま表示する列では、まずこの方法を検討するのがおすすめです。
表示を加工する場合もエスケープする
columns.renderで表示を加工したい場合も、元データをそのままHTMLに埋め込まないようにします。
たとえば、ユーザー名に敬称を付けて表示したい場合です。
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
new DataTable('#usersTable', {
data: users,
columns: [
{
data: 'name',
title: '名前',
render: function (data, type) {
if (type !== 'display') {
return data;
}
return `${escapeHtml(data)} さん`;
}
},
{
data: 'role',
title: '権限',
render: DataTable.render.text()
}
]
});
表示用にHTML文字列を返す場合は、外部から来た値を必ずエスケープしてから埋め込みます。
検索やソートでは元の値を使いたい場合があるため、typeを見てdisplayのときだけ表示用HTMLを返すと扱いやすくなります。
安全な値だけHTMLにする
ステータス表示のように、こちらで定義した固定値だけをHTMLにする場合は比較的安全です。
const STATUS_LABELS = {
active: '有効',
stopped: '停止'
};
const STATUS_CLASSES = {
active: 'status-active',
stopped: 'status-stopped'
};
function renderStatus(data, type) {
const label = STATUS_LABELS[data] ?? '不明';
if (type === 'display') {
const className = STATUS_CLASSES[data] ?? 'status-unknown';
return `<span class="${className}">${label}</span>`;
}
return label;
}
この例では、HTMLに入れるclassNameも表示ラベルも、コード側で定義した値だけを使っています。
DBやユーザー入力の値をそのままclassやhref、styleに入れないようにしましょう。
リンクを作る場合の注意
URLを使ってリンクを作る場合も注意が必要です。
function isSafeUrl(value) {
try {
const url = new URL(value, location.origin);
return url.protocol === 'https:' || url.protocol === 'http:';
} catch {
return false;
}
}
function renderLink(data, type) {
if (type !== 'display') {
return data;
}
if (!isSafeUrl(data)) {
return '';
}
const safeUrl = escapeHtml(data);
return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer">詳細</a>`;
}
javascript:のようなURLをそのままhrefに入れると危険です。
リンクを作る場合は、URLとして許可する形式を確認してからHTMLに埋め込みます。
Ajaxで取得したデータも信用しすぎない
DataTablesではAjaxでデータを取得して表示することも多いです。
new DataTable('#usersTable', {
ajax: '/api/users',
columns: [
{
data: 'name',
title: '名前',
render: DataTable.render.text()
},
{
data: 'email',
title: 'メール',
render: DataTable.render.text()
}
]
});
APIから返ってくるデータであっても、ユーザー入力が混ざる可能性があるなら、表示時にエスケープします。
サーバー側で入力チェックを行い、フロントエンド側でも出力時にエスケープする、という両方の対策を意識すると安全です。
CSRFにも注意する
DataTablesのセキュリティで意識したいのはXSSだけではありません。
DataTablesでAjax更新や削除などの処理を行う場合は、CSRF対策も必要です。
new DataTable('#usersTable', {
ajax: {
url: '/api/users',
headers: {
'X-CSRF-Token': csrfToken
}
},
columns: [
{
data: 'name',
title: '名前',
render: DataTable.render.text()
}
]
});
CSRFトークンの作り方や検証方法は、利用しているフレームワークに合わせて実装します。
DataTables側では、Ajaxリクエストにヘッダーやパラメータとしてトークンを渡せます。
まとめ
DataTablesでXSSを避ける基本は、外部から来た値をHTMLとしてそのまま返さないことです。
- ユーザー入力や外部APIの値は
DataTable.render.text()で表示する columns.renderでHTMLを返す場合は、外部値をエスケープするhrefやclassなどの属性に外部値をそのまま入れない- 表示用HTMLに使う値は、できるだけコード側で定義した安全な値に限定する
- Ajaxで取得したデータも信用しすぎない
- 更新系処理を行う場合はCSRF対策も行う
DataTablesは便利に表示を加工できますが、renderでHTMLを返せる分、扱い方を間違えるとXSSの入口になります。
「入力時のチェック」と「表示時のエスケープ」を分けて考えると、安全な一覧画面を作りやすくなります。
コメント