axiosのサプライチェーン攻撃はfetchに変更すれば完全に防げる

axiosのサプライチェーン攻撃とは

axiosのサプライチェーン攻撃とは、JavaScriptのHTTPクライアントライブラリ「axios」が配布経路(サプライチェーン)を通じて改ざんされ、悪意あるコードがユーザーに配布された事件のことを指します。

axiosをfetchに変更すれば完全に防げる

fetchとはJavaScript標準のHTTP通信APIで、サーバーと通信(GET / POST など)を行うための仕組みです。

npmのインストールやアップデートを行わないため、fetchならサプライチェーン攻撃のリスクがゼロになります。

axiosは便利ですが、単純にGETやPOSTなどでデータの取得や送信だけに使用しているケースでは、以下のようなコードを書けば、fetchで代替可能です。

const api = async (
  method,
  url,
  { params, body, headers, timeout = 30000 } = {}
) => {
  const lowerMethod = method.toLowerCase()
  const METHODS = [
    'get',
    'post',
    'put',
    'patch',
    'delete',
    'head',
    'options'
  ]

  if (!METHODS.includes(lowerMethod)) {
    throw new Error(`Invalid method: ${method}`)
  }

  let fullUrl = url

  if (params) {
    const query = new URLSearchParams(params)
    fullUrl += `?${query}`
  }

  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), timeout)

  const config = {
    method: lowerMethod.toUpperCase(),
    signal: controller.signal,
    headers: {
      'Content-Type': 'application/json',
      ...(headers || {})
    }
  }

  const hasBody = ['post', 'put', 'patch', 'delete'].includes(lowerMethod)

  if (hasBody && body !== undefined) {
    config.body = JSON.stringify(body)
  }

  try {
    const response = await fetch(fullUrl, config)

    if (!response.ok) {
      const text = await response.text()
      throw new Error(`HTTP ${response.status}: ${text}`)
    }

    if (lowerMethod === 'head') {
      return {
        ok: response.ok,
        status: response.status,
        headers: Object.fromEntries(response.headers.entries())
      }
    }

    if (response.status === 204 || lowerMethod === 'options') {
      return null
    }

    return await response.json()
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error(`Request timeout after ${timeout}ms`)
    }
    throw error
  } finally {
    clearTimeout(timer)
  }
}

const result = await api('get', 'https://dummyjson.com/products/search', {
  params: { q: 'phone', limit: 3 }
})

const phones = result.products.map((item) => item.title)
console.log(phones)
// [
//   "Apple AirPods Max Silver",
//   "Apple iPhone Charger",
//   "Apple MagSafe Battery Pack"
// ]

axiosに比べてコードは長く見えますが、API関数を別ファイルにexportし、importして使用すればコード量に大きな差はありません。

サンプルのコードはGETで書きましたが、POSTなども以下のように書けば使用できます。

import { api } from './api.ts'

// GET
await api('get', '/api/users', {
  params: { page: 1 }
})

// POST
await api('post', '/api/users', {
  body: { name: 'John' }
})

// PUT
await api('put', '/api/users/1', {
  body: { name: 'Updated' }
})

// PATCH
await api('patch', '/api/users/1', {
  body: { name: 'Partial Update' }
})

// DELETE
await api('delete', '/api/users/1')

// HEAD
const head = await api('head', '/api/users')
console.log(head.headers)

// OPTIONS
await api('options', '/api/users')

ほとんどのプロジェクトではaxiosを使う必要がなく、fetchで十分なケースが多いため、セキュリティの向上のためにもaxiosを使用する必要性がなければ、fetchを使用することを推奨します。

axiosの代わりにfetchを使用したサンプル