MechaToraのブログ

記事のアイキャッチ画像
技術・開発

個人開発で押さえるべきWebセキュリティ基礎

「個人開発だからセキュリティは後回しでいい」——そう考えていませんか?実は、個人開発のWebアプリでも、脆弱性があれば攻撃の標的になります。ユーザー情報の漏洩、サイト改ざん、サーバー乗っ取りなど、深刻な被害につながる可能性があります。

この記事では、MechaToraで15個のツールを運用する中で学んだ、最低限押さえるべきWebセキュリティ対策を紹介します。XSS、CSRF、SQLインジェクションなどの基本的な脆弱性と、その対策を実際のコード例で解説します。

Webセキュリティが重要な理由

個人開発でも狙われる

「アクセス数が少ないから大丈夫」は間違いです。攻撃者は:

実際に受けた攻撃

MechaToraで運用中に検知した攻撃の例:

幸い、適切な対策により被害はありませんでしたが、対策なしでは危険でした。

OWASP Top 10 - 最も危険な脆弱性

セクション画像

OWASP(Open Web Application Security Project)が発表する、代表的な脆弱性トップ10:

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

この記事では、個人開発で特に重要な上位3つを詳しく解説します。

1. XSS(Cross-Site Scripting)

XSSとは

悪意あるスクリプトをWebページに埋め込む攻撃です。ユーザーの入力をそのまま表示すると発生します。

脆弱性のあるコード例

// 危険なコード(XSS脆弱性あり)
const userName = document.getElementById('name-input').value;
document.getElementById('greeting').innerHTML = `こんにちは、${userName}さん!`;

// ユーザーが「」と入力すると、
// スクリプトが実行されてしまう

対策1:textContent を使う

// 安全なコード
const userName = document.getElementById('name-input').value;
document.getElementById('greeting').textContent = `こんにちは、${userName}さん!`;

// textContent はHTMLタグをエスケープするため、
// スクリプトは実行されず、文字列として表示される

対策2:サニタイゼーション(無害化)

// HTMLを含む場合はサニタイゼーション
function sanitizeHTML(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

const userComment = document.getElementById('comment').value;
const sanitized = sanitizeHTML(userComment);
document.getElementById('display').innerHTML = sanitized;

対策3:DOMPurifyライブラリ

より堅牢な対策には、DOMPurifyを使います:

// DOMPurifyをインストール
// npm install dompurify

import DOMPurify from 'dompurify';

const userInput = '';
const clean = DOMPurify.sanitize(userInput);
document.getElementById('content').innerHTML = clean;

// 悪意あるスクリプトは削除され、安全なHTMLのみ残る

MechaToraでの実装例

日本酒検索アプリでのユーザー入力処理:

function displaySearchResults(items) {
  const resultsDiv = document.getElementById('results');
  resultsDiv.innerHTML = ''; // 一旦クリア

  items.forEach(item => {
    const card = document.createElement('div');
    card.className = 'result-card';

    // textContent で安全に表示
    const title = document.createElement('h3');
    title.textContent = item.itemName; // XSS対策

    const price = document.createElement('p');
    price.textContent = `価格: ${item.itemPrice}円`;

    card.appendChild(title);
    card.appendChild(price);
    resultsDiv.appendChild(card);
  });
}

2. CSRF(Cross-Site Request Forgery)

セクション画像

CSRFとは

ユーザーが意図しない操作を強制的に実行させる攻撃です。ログイン状態のまま悪意あるサイトを訪問すると、勝手にリクエストが送信されます。

攻撃の流れ

  1. ユーザーがWebサイトAにログイン
  2. 攻撃者のサイトBを訪問
  3. サイトBから、サイトAへの削除リクエストが自動送信
  4. ユーザーのデータが削除される

対策1:CSRFトークン

フォーム送信時に、ランダムなトークンを含める:

// サーバー側(Node.js + Express)
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

app.get('/form', csrfProtection, (req, res) => {
  res.render('form', { csrfToken: req.csrfToken() });
});

app.post('/submit', csrfProtection, (req, res) => {
  // CSRFトークンが正しい場合のみ処理
  res.send('データを受け付けました');
});

// HTMLフォーム
<form method="POST" action="/submit">
  <input type="hidden" name="_csrf" value="{{ csrfToken }}">
  <input type="text" name="data">
  <button type="submit">送信</button>
</form>

対策2:SameSite Cookie

CookieにSameSite属性を設定:

// サーバー側でCookie設定
res.cookie('sessionId', sessionId, {
  httpOnly: true,      // JavaScriptからアクセス不可
  secure: true,        // HTTPS のみ
  sameSite: 'strict'   // 同一サイトからのリクエストのみ
});

対策3:カスタムヘッダーの確認

APIリクエストにカスタムヘッダーを要求:

// クライアント側
fetch('/api/delete', {
  method: 'DELETE',
  headers: {
    'X-Requested-With': 'XMLHttpRequest',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ id: 123 })
});

// サーバー側
app.delete('/api/delete', (req, res) => {
  if (req.headers['x-requested-with'] !== 'XMLHttpRequest') {
    return res.status(403).send('Forbidden');
  }
  // 削除処理
});

3. SQLインジェクション

SQLインジェクションとは

SQLクエリに悪意あるコードを挿入して、データベースを不正操作する攻撃です。

脆弱性のあるコード例

// 危険なコード(SQLインジェクション脆弱性あり)
const userId = req.body.userId;
const query = `SELECT * FROM users WHERE id = ${userId}`;
db.query(query, (err, results) => {
  res.json(results);
});

// ユーザーが「1 OR 1=1」と入力すると、
// 全ユーザーの情報が取得されてしまう

対策:プレースホルダー(プリペアドステートメント)

// 安全なコード
const userId = req.body.userId;
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId], (err, results) => {
  res.json(results);
});

// プレースホルダーにより、入力値は文字列として扱われる
// 「1 OR 1=1」も単なる文字列として検索されるため安全

ORMの活用

Sequelize、Prismaなどのスーパーマを使うと、自動的にエスケープされます:

// Sequelize を使った安全な実装
const user = await User.findOne({
  where: { id: userId }
});

// Prisma を使った安全な実装
const user = await prisma.user.findUnique({
  where: { id: userId }
});

// 内部でプリペアドステートメントが使われるため安全

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

4. HTTPS の強制

HTTPSを使わないと、通信内容が盗聴されます:

// Node.js + Expressで HTTPSリダイレクト
app.use((req, res, next) => {
  if (req.headers['x-forwarded-proto'] !== 'https' && process.env.NODE_ENV === 'production') {
    return res.redirect('https://' + req.headers.host + req.url);
  }
  next();
});

GitHub Pagesは自動的にHTTPS化されるため、追加設定は不要です。

5. CORS(Cross-Origin Resource Sharing)設定

他のドメインからのAPIアクセスを制限:

// 全てのドメインを許可(危険)
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  next();
});

// 特定ドメインのみ許可(安全)
const allowedOrigins = ['https://mechatora.com', 'https://blog.mechatora.com'];
app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
  }
  next();
});

6. レート制限

DDoS攻撃やブルートフォース攻撃を防ぐ:

// express-rate-limit を使用
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 100, // 最大100リクエスト
  message: 'リクエストが多すぎます。15分後に再度お試しください。'
});

app.use('/api/', limiter);

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

helmet.jsで主要なセキュリティヘッダーを一括設定:

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"]
    }
  },
  hsts: {
    maxAge: 31536000, // 1年間 HTTPS 強制
    includeSubDomains: true
  }
}));

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

開発時

デプロイ前

運用時

セキュリティ診断ツール

1. OWASP ZAP

2. npm audit

# 依存パッケージの脆弱性チェック
npm audit

# 自動修正
npm audit fix

# 強制的に修正(破壊的変更の可能性あり)
npm audit fix --force

3. Lighthouse(Chrome DevTools)

実際のセキュリティ対策例(MechaTora)

実装した対策

  1. 全ページHTTPS化:GitHub Pagesの標準機能
  2. XSS対策:全てのユーザー入力をtextContentで表示
  3. CSP設定:インラインスクリプトを禁止
  4. レート制限:Cloudflareの無料プランで実装
  5. 依存パッケージ更新:月1回npm audit実行

セキュリティインシデントゼロの維持

2年間の運用で、以下の攻撃を防ぎました:

全て自動的にブロックされ、サービス停止ゼロを達成しています。

まとめ

個人開発でも最低限押さえるべきセキュリティ対策:

必須対策(これだけは必ず)

  1. XSS対策textContentを使う、またはDOMPurifyでサニタイズ
  2. SQLインジェクション対策:プリペアドステートメント、またはORM使用
  3. HTTPS化:GitHub Pagesなら自動、自前サーバーならLet's Encrypt
  4. 依存パッケージ更新npm auditを定期実行

推奨対策(できれば実装)

  1. CSRF対策:トークン、SameSite Cookie
  2. セキュリティヘッダー:helmet.js
  3. レート制限:express-rate-limit
  4. CORS設定:許可ドメインを明示

セキュリティは「後からやろう」ではなく、最初から組み込むべきです。一度攻撃を受けると、復旧に膨大な時間がかかります。最低限の対策だけでも実装して、安全なWebアプリを作りましょう!