設計目標
Valineは、洗練されたスタイル、シンプルな操作、効率的なデプロイを備えたコメントシステムです。初めて触れたとき、その洗練されたスタイルとサーバーレスという特徴に惹かれました。バックエンドサービスは不要で、フロントエンドがLeanCloudのストレージサービスと直接やり取りするのは本当にクールです!しかし、理解を深めるにつれて、その問題点が分かってきました。
Valineの問題点
オープンソースではない
作者はバージョン1.4.0以降、コンパイル済みのファイルのみをGitHubリポジトリにプッシュしており、ソースコードの更新は止まっています。おそらく作者はオープンソースに心を痛めたのでしょう。しかし、私のようにプロジェクトに手を加えたいユーザーにとっては、これは少し厄介な問題です。
XSS
ごく初期のバージョンから、ValineのXSS問題はユーザーによって報告されており、コミュニティもさまざまな方法でこれらの問題を修正してきました。検証コードの追加やフロントエンドでのXSSフィルタリングなどです。しかし、作者は後にフロントエンドでの検証は所詮「君子を防ぐのみ」であることに気づき、検証コードなどの制限は削除されました。
現在、フロントエンドがコメントを公開すると、MarkdownがHTMLに変換され、LeanCloudに送信される前にフロントエンドでXSSフィルター関数が実行されます。LeanCloudからデータを取得した後は、そのままDOMに挿入されます。明らかに、このプロセスには問題があります。HTMLを直接送信し、取得したHTMLをそのまま表示する限り、XSSを根本的に排除することはできません。
根本的な解決策
保存型XSS攻撃に対しては、HTMLコードをエスケープすることで恒久的に解決できます。昔のBBCodeのように、データベースにはMarkdownの内容のみを送信します。フロントエンドは内容を読み取り、Markdown変換後に表示する前に、すべてのHTMLをエンコードします。
function encodeForHTML(str) {
return ('' + str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/');
}Valineはサーバーレスシステムであるため、攻撃者はストレージの段階に直接到達できます。データ保存前の対策はすべて無効で、表示処理の過程でしか対処できません。エスケープ後はすべてのHTMLを解析できないため、変換されたHTMLが挿入される余地はないと言えます。
Valineはもはやオープンソースではないため、Pull Requestを送ることはできません。
上記の方法ではユーザーをMarkdownの範囲に完全に制限してしまうため、Walineは0.15.0以降、クライアント側にDOMPurifyを追加してXSSを防いでいます。通常のXSS無害化に加えて、次の処理も行います:
<form>と<input>は無効化されます- スタイルの注入は無効化されます
- メディアの自動再生は無効化されます
- すべての外部リンクは処理され、relに
noopener noreferrerを設定した新しいウィンドウで開かれます。
プライバシーの漏洩
ストレージに直接アクセスできるだけでなく、攻撃者は任意のデータを直接読み取ることもできます。データベースのフィールドに全員への読み取り権限がある場合、そのフィールドの内容は攻撃者にとって完全に透明です。
コメントデータにおいて、IPとメールアドレスという2つのフィールドにはユーザーの機密データが含まれています。Mr.Dengはこの問題を批判する記事を書いています:Valine.jsのプライバシー漏洩が修正されるまで、すぐに使用を中止してください。JueJinコミュニティが初期にLeanCloudを使用していた際にも、ユーザーの携帯電話番号が漏洩するセキュリティ問題が明るみに出ました。
この問題を回避するため、Valineの作者はユーザーIPの記録を許可するかどうかを設定するrecordIP設定を追加しました。サーバーがないため、値を保存しないことでしか解決できません。
このオプションにはまだ問題があります。IPを記録するかどうかはサイト所有者の設定に依存し、コメントしたユーザーには自分のプライバシーを管理する権利がありません。
メールアドレスの漏洩は、もう1つの主要なプライバシー問題です。Gravatarのアバターを取得するために、フロントエンドでユーザーのメールアドレスのmd5を計算して送信することは完全に実現可能です。しかし、コメントへの返信時にメール通知を送信する必要がある場合は、ユーザーのメールアドレスの元の値を保存せざるを得ません。この問題は理論的にはRSA暗号化で解決できます。秘密鍵はLeanCloudの環境変数に保存できます。クライアントはメールアドレスのmd5と、公開鍵で暗号化したメールアドレスの両方を送信します。LeanCloudがメール通知を送信するとき、クラウド関数で環境から秘密鍵を読み取り、復号してユーザーのメールアドレスを取得します。ただし、フロントエンドのRSA暗号化ライブラリのサイズとパフォーマンスを考慮すると、現実的ではありません。サーバー層を追加し、サーバー側で機密情報をフィルタリングする方が間違いなく優れた方法です。
閲覧数統計の改ざん
Valine 1.2.0では記事の閲覧数統計機能が追加され、ユーザーがページにアクセスすると、バックグラウンドのカウンターテーブルにあるurlに従って訪問数が記録されます。ページにアクセスするたびにデータを更新する必要があるため、後続のフィールド更新を実行できるように権限を書き込み可能に設定しなければなりません。ここで問題が生じます。実際、データは任意の値に更新できてしまいます。興味があれば、https://valine.js.org/visitor.htmlの公式サイトを開いてコンソールに次のコードを入力し、試してみてください。試した後は数値を元に戻すのをお忘れなく~
const counter = new AV.Query('Counter');
const resp = await counter.equalTo('url', '/visitor.html').find();
resp[0].set('time', -100000).save();
location.reload();幸いなことに、timeフィールドの値はNumber型であるため、他の値を挿入することはできません。もしtimeフィールドが文字列型であれば、XSS脆弱性になっていたかもしれません。この問題の可能な解決策は、累積的な保存方式をやめることです。訪問ごとに読み取り専用のアクセス記録を保存し、読み取り時にcount()メソッドで統計を取る方式に変更します。こうすればすべてのデータが読み取り専用になり、改ざんの問題は解決します。ただし、この解決策にも問題があります。データ量が比較的多くなると、クエリに一定の負荷がかかります。
元のデータを維持する場合は、変更権限を分離するためにサーバー層を追加するしかありません。
Walineの誕生
以上の理由から、Walineが誕生しました。Walineの当初の目的はValineにバックエンドを追加することだけでしたが、Valineがオープンソースではないため、フロントエンドで実装するしかありませんでした。もちろん、Valineの設定との一貫性を保つため、フロントエンドの多くのコードとロジックはValineを参考にしています。プロジェクト名さえValineから派生させているので、誰でもこのプロジェクトがValineの派生であることを理解できるでしょう。
上記のセキュリティ問題を解決するだけでなく、サーバー側を追加したことで、従来はサーバーがないために制限されていた多くの機能(メール通知、スパムコメントのフィルタリングなど)を実装できるようになりました。
