個人開発でWebアプリをどんどん出す人向け。出す前の5分で通せる最低限の衛生と、機能を足したときだけ発火させる追加対策を、コピペできる形でまとめた。
なぜ「自分の小さなアプリ」にも要るのか
「お金もユーザーもいないアプリは狙われない」とよく聞く。これは誤解だ。
現代の攻撃の主流は、人が選ぶ標的型ではなく、botによる無差別・自動スキャンである。攻撃者はネット全体を機械的に走査し、既知の穴を見つけ次第に侵入する。狙われる理由は「お金」ではなく「届く・自動化できる」からだ。
小さなアプリでも、乗っ取られれば 踏み台・スパム中継・マイニング・フィッシング配信 に使われる。ユーザーのメールやパスワード(使い回しに価値がある)も盗まれる。「お金がない=安全」は成り立たない。狙われているのは銀行残高ではなく、サーバーとユーザーのデータだ。
AIでアプリを量産できる時代ほど、これは効いてくる。生成は速いが、生成物の安全は誰も保証してくれない。出す数が増えるほど、出す前の最低限が雑になりやすい。
考え方:リスク = 資産 × 確率 × 影響
全部を完璧にやる必要はない。守るべき重さは「持っている資産 × 攻撃される確率 × 壊れたときの影響」で決まる。
- 高価な対策(ペンテスト / WAF / 監視)は、守る物(ユーザー・データ)ができてから。
- 安いベースライン衛生は、初日から。自動・無差別攻撃は“定数”として常にかかるからだ。
この記事の A が初日からの定数、B が機能追加で発火するトリガーである。
A. 出す前5分の「非交渉」チェック
新規アプリごとに、上から通すだけでいい。
-
秘密情報をコード/リポに置かない(
.envを gitignore・鍵の直書きゼロ) - HTTPSのみ(多くのPaaSは自動。HTTP→HTTPSリダイレクトを確認)
-
セキュリティヘッダ一括(Express:
helmet()) - リクエストサイズ制限(巨大ボディ拒否)
- 基本レート制限
-
依存を最新&監査(
npm audit+ Dependabot) - エラーで内部情報を漏らさない(stack traceをユーザーに返さない)
-
CORSは具体オリジンだけ(
*を使わない・特にcredentials時) - ログに秘密/PIIを出さない
- 本番にデバッグ/管理画面を晒さない
B. 機能を足したら“発火”するトリガー
| 足す機能 | 発火する対策 |
|---|---|
| 認証/ログイン | パスワードは bcrypt/argon2 でハッシュ・自作認証しない・cookieは HttpOnly + Secure + SameSite
|
| DB | パラメータ化クエリ(文字列連結SQL禁止=SQLi防止)・DBユーザは最小権限 |
| ユーザー入力の表示 | 出力エスケープ(XSS防止)・入力バリデーション |
| 個人データ取得 | ↓「個人情報トリガー」(法的義務が立つ) |
| 決済 | 自前でカード情報を持たない(Stripe等に委譲)・PCI範囲を外に出す |
| Webhook/外部連携 | 署名検証必須(送信元が本物か確認) |
| ファイルアップロード | 種別/サイズ制限・実行可能領域に置かない |
個人データを取った瞬間(個人情報保護法)
- 取得は最小限・利用目的を明示・プライバシーポリシー設置
- 安全管理措置(アクセス制御・暗号化・アクセスログ)
- 漏洩時の報告ルート(個人情報保護委員会への報告+本人通知が、一定の場合は義務)
C. コピペ資産(Node/Express)
.gitignore
.env
.env.*
node_modules/
*.pem
*.key
セキュリティヘッダ+サイズ制限+レート制限
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
app.use(helmet()); // CSP/HSTS/X-Content-Type-Options 等を一括
app.use(express.json({ limit: '100kb' })); // 巨大ボディ拒否
app.use(rateLimit({ windowMs: 60_000, max: 60 })); // 1分60回まで
セッションcookie
res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'lax' });
パスワードのハッシュ
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, 12);
const ok = await bcrypt.compare(input, hash);
パラメータ化クエリ(SQLi防止)
// ❌ `SELECT * FROM users WHERE id = '${id}'`
// ✅
db.query('SELECT * FROM users WHERE id = $1', [id]);
エラーで内部を漏らさない
app.use((err, req, res, _next) => {
console.error(err); // ログには残す
res.status(500).json({ error: 'Internal Server Error' }); // ユーザーには詳細を出さない
});
Webhook 署名検証(生ボディで計算・タイミング安全に比較)
import crypto from 'crypto';
function verifySignature(rawBody, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('base64');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
// 署名ヘッダと比較。raw body を使う(JSON.parse 後ではダメ)。
Dependabot(.github/dependabot.yml)
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
まとめ
高価な対策 → 守る物(ユーザー/データ)に合わせて後で
ベースライン衛生 → 初日から(安い・自動攻撃に常時さらされる)
個人データ → 持った瞬間に法的義務とリスク移転(保険/委譲)を真剣に
量産しても、各アプリが裸で出ていかないように。A を5分で通し、機能を足すたびに B を発火させる。これだけで、致命的な事故のほとんどは初日に防げる。
思想・構成・校閲:cmalu ractu
文章生成:Claude(Anthropic)