😥

API叩くときはしっかりデコードしよう

2024/08/12に公開

概要

Hono + LINE Messaging APIを使ってコンビニスイーツをランダムに提供するLINE BOTを動かしていたら、あるときローソンの新商品が取得できない事象が発生しました。


ローソンの新商品と入力すれば、新商品が返信されるのに返信されない

LINE Messaging APIのエラーレスポンスを見ると下記メッセージが送信されており、リクエストボディの内容が不正だったことが分かります。

A message (messages[0]) in the request body is invalid
(: リクエストボディのメッセージ(messages[0])が無効である。)

結論

デコード処理が充分ではなかったことが原因でした。
このLINE BOTでは事前にセブンイレブン、ローソン、ファミリーマートの新商品情報をCloudflare Workers KVに登録しておき、「〇〇(コンビニ名)の新商品」とLINEからメッセージを送信することでKVから新商品の情報を返却します。

KVへの事前登録時にはHTMLRewriter()を使用して各企業ページのhtmlを解析してKVに登録しておりますが、htmlのデコード処理が充分ではなかったことが原因です。

parseName(text: string): string {
    return text
      .replace(/"/g, '"')
      .replace(/&/g, '&')
      .replace(/&lt;/g, '<')
      .replace(/&gt;/g, '>')
      .replace(/&nbsp;/g, ' ')
      .trim();
}

リクエストボディを見返してみる

正常終了したセブンイレブンのリクエストボディとローソンのリクエストボディを比較して見ると、明らかにおかしい文字(&eacute;)がいますね...

セブンイレブン.json
[
  {
    "id": "SevenEleven109ace55-73dd-4d84-8a3e-17df30884325",
    "itemName": "ホイップで食べる コーヒーわらび餅",
    "itemPrice": "270円(税込291.60円)",
    "itemImage": "https://img.7api-01.dp1.sej.co.jp/item-image/112483/F85269A8CE1D29DE2AA4012549A9D8A6.jpg",
    "itemHref": "https://www.sej.co.jp/products/a/item/112483/kanto/",
    "storeType": "SevenEleven",
    "metadata": {
      "isNew": true,
      "releasePeriod": "this_week"
    }
  },
  {
    "id": "SevenEleven62cac584-88bf-4a74-b758-e164cba77c31",
    "itemName": "ホイップだけどら",
    "itemPrice": "178円(税込192.24円)",
    "itemImage": "https://img.7api-01.dp1.sej.co.jp/item-image/112482/C81899C7EB0877B9423CF47CCEDFF31C.jpg",
    "itemHref": "https://www.sej.co.jp/products/a/item/112482/kanto/",
    "storeType": "SevenEleven",
    "metadata": {
      "isNew": true,
      "releasePeriod": "this_week"
    }
  }
]
ローソン.json
[
  {
    "id": "Lawson0c75b876-b3d1-4c5a-8d03-3cf9b2c1b5c3",
    "itemName": "Uchi Caf&eacute;×猿田彦珈琲 カフェラテロールケーキ(コーヒーゼリー入り)",
    "itemPrice": "268円(税込)",
    "itemImage": "https://www.lawson.co.jp/recommend/original/detail/img/l763344.jpg",
    "itemHref": "https://www.lawson.co.jp/recommend/original/detail/1490171_1996.html",
    "storeType": "Lawson",
    "metadata": {
      "isNew": true,
      "releasePeriod": "this_week"
    }
  },
  {
    "id": "Lawson101f97c7-d5de-49d9-ae0d-0247c3c8d6e9",
    "itemName": "Uchi Caf&eacute;×猿田彦珈琲 カフェラテどらもっち",
    "itemPrice": "235円(税込)",
    "itemImage": "https://www.lawson.co.jp/recommend/original/detail/img/l760181.jpg",
    "itemHref": "https://www.lawson.co.jp/recommend/original/detail/1490172_1996.html",
    "storeType": "Lawson",
    "metadata": {
      "isNew": true,
      "releasePeriod": "this_week"
    }
  }
]

解決策

手動デコードを行っていたため、HTMLエンティティのエンコードとデコードをサポートするhtml-entitiesを導入しました。

pnpm add html-entities
parseName(text: string): string {
    return decode(text).trim();
}

再度「ローソンの新商品」を送信したところ、正常にデコードできていることが確認できました🎉

学び

デコードされていない文字列をAPIで許可してしまうことは、一般的にセキュリティ上のリスクを伴います。

クロスサイトスクリプティング(XSS)のリスク

デコードされていない文字列がそのまま受け入れられると、特にWebアプリケーションではXSS攻撃に対して脆弱になる可能性があります。
例えば、<script>タグや他のHTMLエンティティがエスケープされないままブラウザでレンダリングされると、悪意のあるスクリプトが実行されてしまう危険があります。

SQLインジェクションのリスク

デコードされていない文字列がAPIを通じてデータベースに渡された場合、SQLインジェクションのリスクが高まります。
これは、攻撃者がデコードされていない文字列を利用して、データベースのクエリを不正に操作する可能性があるためです。

データ汚染のリスク

デコードされていない文字列がデータベースや他のストレージシステムに保存されると、後で取り出した際にデータが汚染されている可能性があります。
これにより、アプリケーションの正常な動作が妨げられるだけでなく、さらにセキュリティリスクが生じる可能性があります。

不正なリクエスト操作のリスク

デコードされていない文字列がそのままAPIで許可されると、攻撃者が意図的に特殊文字を含むリクエストを送信し、APIの動作を不正に操作するリスクがあります。
特にURLやクエリパラメータ内での処理において、このリスクは顕著です。

まとめ

デコード処理の不足が原因で、APIリクエストが不正とされる問題に直面しましたが、適切なデコード処理を導入することで解決に至りました。
この経験から学べるのは、APIやWebアプリケーションを開発する際にはデータの取り扱いにおいて細心の注意が必要ですね。

以上になります👊

Discussion