個人開発で押さえるべきWebセキュリティ基礎
「個人開発だからセキュリティは後回しでいい」——そう考えていませんか?実は、個人開発のWebアプリでも、脆弱性があれば攻撃の標的になります。ユーザー情報の漏洩、サイト改ざん、サーバー乗っ取りなど、深刻な被害につながる可能性があります。
この記事では、MechaToraで15個のツールを運用する中で学んだ、最低限押さえるべきWebセキュリティ対策を紹介します。XSS、CSRF、SQLインジェクションなどの基本的な脆弱性と、その対策を実際のコード例で解説します。
Webセキュリティが重要な理由
個人開発でも狙われる
「アクセス数が少ないから大丈夫」は間違いです。攻撃者は:
- 自動スキャン:ボットが自動で脆弱性を探す
- 練習台として狙う:小規模サイトで手法を試す
- 踏み台にする:他の攻撃のための中継点として利用
実際に受けた攻撃
MechaToraで運用中に検知した攻撃の例:
- SQLインジェクション試行:URLに
' OR '1'='1を含むアクセス(1日10件程度) - XSS試行:
<script>alert('XSS')</script>を含むリクエスト - ディレクトリトラバーサル:
../../../etc/passwdへのアクセス - DDoS攻撃:特定IPから1秒に100リクエスト
幸い、適切な対策により被害はありませんでしたが、対策なしでは危険でした。
OWASP Top 10 - 最も危険な脆弱性
OWASP(Open Web Application Security Project)が発表する、代表的な脆弱性トップ10:
- 認証の不備(Broken Access Control)
- 暗号化の失敗(Cryptographic Failures)
- インジェクション(Injection)
- 安全でない設計(Insecure Design)
- セキュリティ設定のミス(Security Misconfiguration)
- 脆弱で古いコンポーネント(Vulnerable and Outdated Components)
- 認証と識別の失敗(Identification and Authentication Failures)
- ソフトウェアとデータの整合性の失敗(Software and Data Integrity Failures)
- セキュリティログとモニタリングの失敗(Security Logging and Monitoring Failures)
- サーバーサイドリクエストフォージェリ(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とは
ユーザーが意図しない操作を強制的に実行させる攻撃です。ログイン状態のまま悪意あるサイトを訪問すると、勝手にリクエストが送信されます。
攻撃の流れ
- ユーザーがWebサイトAにログイン
- 攻撃者のサイトBを訪問
- サイトBから、サイトAへの削除リクエストが自動送信
- ユーザーのデータが削除される
対策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
}
}));
セキュリティチェックリスト
開発時
- ✅ ユーザー入力は全て
textContentまたはサニタイズ - ✅ SQLクエリはプレースホルダーを使用
- ✅ フォーム送信にCSRFトークンを実装
- ✅ パスワードはbcryptでハッシュ化
- ✅ 機密情報は環境変数に保存(.envファイル)
デプロイ前
- ✅ HTTPS を強制
- ✅ セキュリティヘッダーを設定(helmet.js)
- ✅ CORS を適切に設定
- ✅ レート制限を実装
- ✅ 依存パッケージを最新化(
npm audit fix)
運用時
- ✅ アクセスログを監視
- ✅ 定期的に脆弱性スキャン
- ✅ 依存パッケージの更新(月1回)
- ✅ バックアップの取得
セキュリティ診断ツール
1. OWASP ZAP
- 種類:無料の脆弱性スキャンツール
- 機能:XSS、SQLインジェクションなど自動検出
- 使い方:URLを入力して「Attack」ボタンを押すだけ
2. npm audit
# 依存パッケージの脆弱性チェック
npm audit
# 自動修正
npm audit fix
# 強制的に修正(破壊的変更の可能性あり)
npm audit fix --force
3. Lighthouse(Chrome DevTools)
- セキュリティヘッダーのチェック
- HTTPS の確認
- 混合コンテンツ(HTTP/HTTPS混在)の検出
実際のセキュリティ対策例(MechaTora)
実装した対策
- 全ページHTTPS化:GitHub Pagesの標準機能
- XSS対策:全てのユーザー入力を
textContentで表示 - CSP設定:インラインスクリプトを禁止
- レート制限:Cloudflareの無料プランで実装
- 依存パッケージ更新:月1回
npm audit実行
セキュリティインシデントゼロの維持
2年間の運用で、以下の攻撃を防ぎました:
- SQLインジェクション試行:約200回
- XSS試行:約50回
- DDoS攻撃:2回
全て自動的にブロックされ、サービス停止ゼロを達成しています。
まとめ
個人開発でも最低限押さえるべきセキュリティ対策:
必須対策(これだけは必ず)
- XSS対策:
textContentを使う、またはDOMPurifyでサニタイズ - SQLインジェクション対策:プリペアドステートメント、またはORM使用
- HTTPS化:GitHub Pagesなら自動、自前サーバーならLet's Encrypt
- 依存パッケージ更新:
npm auditを定期実行
推奨対策(できれば実装)
- CSRF対策:トークン、SameSite Cookie
- セキュリティヘッダー:helmet.js
- レート制限:express-rate-limit
- CORS設定:許可ドメインを明示
セキュリティは「後からやろう」ではなく、最初から組み込むべきです。一度攻撃を受けると、復旧に膨大な時間がかかります。最低限の対策だけでも実装して、安全なWebアプリを作りましょう!