• #security
  • #web
  • #best-practices
未分類

個人開発で意識したセキュリティ対策

個人開発だからといってセキュリティを疎かにすると、ユーザーに迷惑をかけるだけでなく自分の信用も失う。最低限押さえるべきポイントをまとめる。

OWASP Top 10 を意識する

Webアプリケーションにおけるセキュリティリスクのトップ10である。これを知っているだけで大半の脆弱性は防げる。

順位リスク概要
1Broken Access Control認可の不備
2Cryptographic Failures暗号化の失敗
3InjectionSQLi, XSS, コマンドインジェクション
4Insecure Design設計段階の問題
5Security Misconfiguration設定ミス
6Vulnerable Components脆弱な依存関係
7Authentication Failures認証の不備
8Software Integrity Failures整合性の問題
9Logging Failuresログ・監視の不備
10SSRFサーバーサイドリクエストフォージェリ

1. 入力値は全て信用しない

大原則: ユーザーからの入力は全て悪意があると仮定する。

SQLインジェクション対策

// NG: 文字列結合
const query = `SELECT * FROM users WHERE id = ${userId}`;

// OK: プリペアドステートメント
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId]);

ORMを使っていても、生SQLを書く箇所は要注意。

XSS対策

// NG: innerHTML に直接代入
element.innerHTML = userInput;

// OK: textContent を使う
element.textContent = userInput;

// または適切にエスケープ
function escapeHtml(str) {
  return str
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

Reactなどのフレームワークはデフォルトでエスケープするが、dangerouslySetInnerHTMLは要注意である。

コマンドインジェクション対策

// NG: ユーザー入力をそのままコマンドに
exec(`convert ${filename} output.png`);

// OK: 引数として渡す
execFile('convert', [filename, 'output.png']);

2. 認証・認可

パスワードの保存

// NG: 平文やMD5
const password = userInput; // 平文
const hash = md5(password); // 弱いハッシュ

// OK: bcrypt や Argon2
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(password, 12); // コスト12以上

セッション管理

  • セッションIDは十分にランダムに(暗号学的乱数生成器を使う)
  • ログイン後はセッションIDを再生成(セッション固定攻撃対策)
  • HttpOnly, Secure, SameSite 属性を設定
// Express の例
app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: {
    httpOnly: true,
    secure: true, // HTTPS必須
    sameSite: 'strict',
    maxAge: 24 * 60 * 60 * 1000 // 1日
  },
  resave: false,
  saveUninitialized: false
}));

JWT を使う場合

  • 秘密鍵は十分に長く(256bit以上)
  • alg: none 攻撃に注意(ライブラリで明示的にアルゴリズムを指定)
  • 有効期限を短めに設定
  • リフレッシュトークンとアクセストークンを分離

3. HTTPS必須

2024年以降、HTTPは論外。

  • Let's Encrypt で無料でSSL証明書取得可能
  • Cloudflare や Vercel を使えば自動で設定される
  • HSTS ヘッダーを設定
Strict-Transport-Security: max-age=31536000; includeSubDomains

4. 環境変数と秘密情報の管理

絶対にやってはいけないこと

// NG: コードにハードコード
const API_KEY = 'sk-1234567890abcdef';
const DB_PASSWORD = 'password123';

正しい管理方法

# .env ファイル(.gitignore に追加必須)
API_KEY=sk-1234567890abcdef
DB_PASSWORD=supersecretpassword
// 環境変数から読み込む
const apiKey = process.env.API_KEY;

.gitignore の設定

.env
.env.local
.env.*.local
*.pem
*.key

git履歴に入ってしまった場合: 秘密鍵は即座にローテーション。履歴から消すのは困難なので、漏れた前提で対応。

5. 依存関係の管理

脆弱性スキャン

# npm
npm audit
npm audit fix

# yarn
yarn audit

# Snyk(より詳細)
npx snyk test

定期的なアップデート

# 更新可能なパッケージを確認
npm outdated

# Dependabot や Renovate を設定して自動PR

lockfileをコミット

package-lock.jsonyarn.lock は必ずコミット。依存関係のバージョンを固定することで、サプライチェーン攻撃のリスクを軽減。

6. エラーハンドリング

本番環境でスタックトレースを見せない

// NG: エラー詳細をそのまま返す
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.stack });
});

// OK: 本番では汎用メッセージ
app.use((err, req, res, next) => {
  console.error(err); // ログには記録
  res.status(500).json({
    error: process.env.NODE_ENV === 'production'
      ? 'Internal Server Error'
      : err.message
  });
});

7. レート制限

ブルートフォース攻撃やDoSへの対策である。

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 100, // 100リクエストまで
  message: 'Too many requests'
});

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

// ログイン試行はより厳しく
const loginLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1時間
  max: 5, // 5回まで
});
app.use('/api/login', loginLimiter);

8. CORS設定

// NG: 全許可
app.use(cors());

// OK: 必要なオリジンのみ
app.use(cors({
  origin: ['https://myapp.com', 'https://www.myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true
}));

9. セキュリティヘッダー

helmetミドルウェアで一括設定する。

const helmet = require('helmet');
app.use(helmet());

設定されるヘッダー:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Content-Security-Policy
  • X-XSS-Protection(レガシーブラウザ向け)

CSP(Content Security Policy)の例

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "https://cdn.example.com"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"],
  }
}));

10. ログと監視

記録すべき情報

  • 認証の成功/失敗
  • 認可エラー
  • 入力バリデーションエラー
  • 例外・エラー

記録してはいけない情報

  • パスワード
  • クレジットカード番号
  • 個人情報(必要最小限に)
// NG
console.log(`Login attempt: user=${email}, password=${password}`);

// OK
console.log(`Login attempt: user=${email}, success=${success}, ip=${req.ip}`);

チェックリスト

個人開発でも最低限これだけは確認:

  • HTTPS を使っている
  • パスワードは bcrypt/Argon2 でハッシュ化
  • SQLはプリペアドステートメント
  • ユーザー入力はエスケープ/サニタイズ
  • .env は .gitignore に入っている
  • 本番でエラー詳細を露出していない
  • npm audit でクリティカルな脆弱性がない
  • Cookie に HttpOnly, Secure, SameSite 設定
  • CORS は必要なオリジンのみ許可
  • レート制限を設定している

面接での回答ポイント

「セキュリティで意識していることは?」

  1. OWASP Top 10 を把握し、特にインジェクション系を意識
  2. 入力値は全て信用しない原則
  3. 秘密情報は環境変数で管理、コードにハードコードしない
  4. 依存関係の脆弱性を定期的にチェック

「具体的にどんな対策をした?」

  • bcrypt でパスワードハッシュ化(コスト12)
  • helmet でセキュリティヘッダー一括設定
  • express-rate-limit でブルートフォース対策
  • npm audit を CI に組み込んで自動チェック

「万が一漏洩したらどうする?」

  1. 影響範囲の特定
  2. 漏洩した認証情報のローテーション
  3. ユーザーへの通知(必要に応じて)
  4. 原因調査と再発防止策の実施