CSV機能にはエスケープとCSVインジェクション対策が必須

CSVファイルダウンロード機能の実装について

WebサイトでCSVダウンロード機能を実装する際、単にデータの値をカンマで連結するだけでCSVファイルを生成・ダウンロードさせているケースをたまに見かけます。

CSVファイルの生成ではエスケープ処理に加えて、CSVインジェクションへの対策を考慮しないと、入力データに含まれる文字列が「不正なCSV」または「計算式」として実行されるリスクが生じます。

CSVのエスケープをしないことによる危険性

CSVのエスケープとは、カンマ( , )、ダブルクォート( " )、改行コード( \n や \r )が含まれる値をダブルクォートで囲み、データ構造が崩れるのを防ぐ処理です。

CSVエスケープを行わない場合、生成されたCSVのデータ構造そのものが壊され、システムの不具合や重大なセキュリティ脆弱性を引き起こします。

カンマがエスケープされていないとデータ構造の崩壊(列・行のズレ)を引き起こし、システムの不具合(エラー)を引き起こす原因になります。

エスケープせずSQL等に投入していた場合は、破壊された文字列データが原因で二次的な脆弱性を誘発する可能性もあります。

以下はエスケープされていないCSVファイルをダウンロードするサンプルです。

JavaScript
document.getElementById('b').addEventListener('click', () => {
  const data = [
    ['id', 'title'],
    [1, '=1+2'],
    [2, '@SUM(1,2)'],
    [3, '-5+3'],
    [4, '=HYPERLINK("https://example.com/", "x")'],
    [5, ','],
  ]
  const csvContent = data
    .map((row) =>
      row
        .map((field) => {
          let str = String(field)
          return str
        })
        .join(','),
    )
    .join('\r\n')
  const bom = new Uint8Array([0xef, 0xbb, 0xbf])
  const blob = new Blob([bom, csvContent], { type: 'text/csv;charset=utf-8;' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = 'sample.csv'
  a.click()
  URL.revokeObjectURL(url)
})

このボタンを押してダウンロードしたCSVファイルをExcelなどで開くと、式が数式として評価されて、カンマの影響で2列が3列になっていることが確認できます。

エスケープしていないCSVをNumbersで開いた結果

[",\n\r] が存在する場合は「"」で囲んでエスケープ

前述の例のようにカンマ( , ) が文字列に含まれる場合はダブルクォーテーションで囲むことで文字列として認識されます。

さらに、CSVではダブルクォーテーション( " )、\n、\r が存在する場合もそのままでは文字列として認識されないのでダブルクォーテーションで囲みます。

ダブルクォーテーションで囲んだ文字列の中にダブルクォーテーションがあると文字列として認識される範囲が変わってしまうので、ダブルクォーテーションで囲んだ文字列の中にダブルクォーテーションがある場合は「"」から「""」に置換してエスケープします。

JavaScript
if (/[",\n\r]/.test(str)) {
  str = `"${str.replace(/"/g, '""')}"`
}

たまにダブルクォーテーションの有無などを判定せずに、すべてのデータをダブルクォーテーションで囲っているCSVファイルを見かけますが、ファイルサイズが無駄に増える問題のあるやり方なので避けてください。

CSVインジェクションの対策も必要

ユーザーが入力したデータに「@SUM(...)」や「=HYPERLINK(...)」といった文字列が含まれていると、生成されたCSVをExcelなどで開いた際に数式として評価されてしまいます。

「=1+2」がExcelで開くと「3」になってしまうのも数式として評価されているからです。

OWASP(Web Application Security Project)のガイドラインでは、信頼できない入力値の先頭が以下の記号で始まっている場合、先頭にシングルクォーテーション(')を付与することを推奨しています。

  • =(イコール)
  • +(プラス)
  • -(マイナス)
  • @(アットマーク)
  • \t(タブ)
  • \r(キャリッジリターン)
  • \n(ラインフィード)

JavaScriptで置換処理を書くと以下のようになります。

JavaScript
if (/^[=+\-@\t\r\n]/.test(str)) {
  str = `'${str}`
}

先頭にシングルクォーテーションを付ければ、Excelなどで開いても数式として評価されません。

CSVをテキストエディタで開くとこのようになっています。
id,title
1,'=1+2
2,"'@SUM(1,2)"
3,'-5+3
4,"'=HYPERLINK(""https://example.com/"", ""x"")"
5,","

Excelの自動変換は回避できない

CSVファイルをExcelで直接開くと、一部のデータが自動的に変換されてしまい、意図しない値に置き換わることがあります。

この自動変換処理はエスケープ処理では完全には回避できないので、注意が必要です。

※ CSVファイルは直接開かず、「データ → データファイル指定 (Power Query)」を選択して、CSVファイルをインポートして確認してください。

まとめ

ユーザーからの任意入力を受け付けるシステムでCSV出力機能を備える場合、そのままではセキュリティ上のリスクが残ります。

不特定多数の方が使用して、CSVファイルをExcelなどの表計算ソフトで開くのであれば、ダブルクォーテーションを使用した文字エスケープとシングルクォーテーションを先頭に追加するCSVインジェクション対策を必ず行ってください。

JavaScript
document.getElementById('b').addEventListener('click', () => {
  const data = [
    ['id', 'title'],
    [1, '=1+2'],
    [2, '@SUM(1,2)'],
    [3, '-5+3'],
    [4, '=HYPERLINK("https://example.com/", "x")'],
    [5, ','],
  ]
  const csvContent = data
    .map((row) =>
      row
        .map((field) => {
          let str = String(field)

          // CSVインジェクション対策
          if (/^[=+\-@\t\r]/.test(str)) {
            str = `'${str}`
          }
          
          // CSVをダブルクォーテーションで囲んでエスケープ
          if (/[",\r\n]/.test(str)) {
            str = `"${str.replace(/"/g, '""')}"`
          }
          
          return str
        })
        .join(','),
    )
    .join('\r\n')
  const bom = new Uint8Array([0xef, 0xbb, 0xbf])
  const blob = new Blob([bom, csvContent], { type: 'text/csv;charset=utf-8;' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = 'sample.csv'
  a.click()
  URL.revokeObjectURL(url)
})