荷物追跡のセキュリティ:配送データとAPIキーの保護
配送データは多くの開発者が認識している以上に機密性が高いものです。送り状番号からは、購入履歴、自宅住所、配達パターン、取引先との関係が明らかになる可能性があります。追跡連携を構築する際には、セキュリティを後回しにするのではなく、最初から重要な要素として取り組むべきです。このガイドでは、荷物追跡データを扱うすべてのアプリケーションに不可欠なセキュリティ対策を解説します。
APIキーのセキュリティ
クライアントサイドのコードにキーを公開しない
最もよくある間違いは、フロントエンドのJavaScriptにAPIキーを埋め込むことです。
// WRONG — API key visible to anyone who opens browser DevTools
const response = await fetch('https://api.whereparcel.com/v2/track', {
headers: { 'Authorization': 'Bearer wp_live_abc123...' },
});
代わりに、リクエストをバックエンド経由でプロキシしましょう。
// Frontend — calls your own API
const response = await fetch('/api/tracking', {
method: 'POST',
body: JSON.stringify({ trackingNumber: '123456789012' }),
});
// Backend — adds API key server-side
app.post('/api/tracking', async (req, res) => {
const result = await fetch('https://api.whereparcel.com/v2/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.WHEREPARCEL_API_KEY}:${process.env.WHEREPARCEL_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
trackingItems: [{ carrier: req.body.carrier, trackingNumber: req.body.trackingNumber }],
}),
});
res.json(await result.json());
});
環境変数を使用する
APIキーはコードではなく環境変数に保存しましょう。
# .env (never commit this file)
WHEREPARCEL_API_KEY=wp_live_abc123...
# .gitignore
.env
.env.local
.env.production
キーを定期的にローテーションする
キーのローテーションスケジュールを設定し、WhereParcelのダッシュボードで複数の有効なキーを管理しましょう。
- 新しいAPIキーを生成する
- 新しいキーをアプリケーションにデプロイする
- 本番環境で新しいキーが動作することを確認する
- 古いキーを無効化する
送り状番号の列挙攻撃の防止
送り状番号は連番や予測可能なパターンに従っていることが多くあります。適切な保護がなければ、攻撃者が送り状番号を列挙して他のお客様の配送データにアクセスする可能性があります。
追跡エンドポイントにレート制限を設定する
import rateLimit from 'express-rate-limit';
const trackingLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 30, // 30 requests per window per IP
message: { error: 'Too many tracking requests, please try again later' },
standardHeaders: true,
});
app.use('/api/tracking', trackingLimiter);
認証を必須にする
可能な限り、匿名での追跡検索を許可しないようにしましょう。
app.post('/api/tracking', authenticate, async (req, res) => {
const { trackingNumber } = req.body;
// Verify this tracking number belongs to the authenticated user
const shipment = await db.shipments.findOne({
trackingNumber,
userId: req.user.id,
});
if (!shipment) {
return res.status(404).json({ error: 'Shipment not found' });
}
// Only then fetch tracking data
const tracking = await getTracking(trackingNumber, shipment.carrier);
res.json(tracking);
});
リクエストのフィンガープリントを追加する
列挙攻撃を検出してブロックします。
async function detectEnumeration(req) {
const key = `tracking:${req.ip}`;
const recentNumbers = await redis.lrange(key, 0, -1);
// Track which numbers this IP has queried
await redis.lpush(key, req.body.trackingNumber);
await redis.ltrim(key, 0, 99); // Keep last 100
await redis.expire(key, 3600); // 1 hour window
// Flag if querying many different tracking numbers
const uniqueNumbers = new Set(recentNumbers).size;
if (uniqueNumbers > 20) {
await flagSuspiciousActivity(req.ip, 'tracking_enumeration');
return true;
}
return false;
}
機密データの保護
データの最小化
必要なもののみを保存しましょう。
// Store minimal tracking data
const trackingRecord = {
trackingNumber: shipment.trackingNumber,
carrier: shipment.carrier,
currentStatus: tracking.status,
lastUpdated: tracking.lastEvent.timestamp,
// DON'T store: full address, recipient name, package contents
};
ログ内の送り状番号をマスクする
アプリケーションログに送り状番号をそのまま出力してはいけません。
function maskTrackingNumber(number) {
if (number.length <= 6) return '***';
return number.slice(0, 3) + '*'.repeat(number.length - 6) + number.slice(-3);
}
// Log: "Tracking lookup for 123******890"
logger.info(`Tracking lookup for ${maskTrackingNumber(trackingNumber)}`);
保存データの暗号化
追跡データをキャッシュする場合は、機密フィールドを暗号化しましょう。
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
function encryptField(text, key) {
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}
Webhookのセキュリティ
Webhook経由で追跡更新を受信する際は、送信元を検証しましょう。
Webhook署名の検証
import crypto from 'crypto';
app.post('/webhooks/tracking', (req, res) => {
const signature = req.headers['x-whereparcel-signature'];
const payload = JSON.stringify(req.body);
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook...
res.status(200).json({ received: true });
});
HTTPSのみを使用する
Webhookエンドポイントには必ずHTTPSを使用してください。HTTPでのWebhook登録は拒否しましょう。
WebhookのソースIPを検証する
オプションとして、WhereParcelのIP範囲からのWebhook処理のみを許可することもできます。
const ALLOWED_IPS = [
// WhereParcel webhook IPs — check docs for current list
'203.0.113.0/24',
];
app.post('/webhooks/tracking', verifySourceIP(ALLOWED_IPS), processWebhook);
データプライバシーの遵守
GDPRへの考慮事項
欧州のお客様にサービスを提供する場合は以下を遵守しましょう。
- データの最小化 — 必要な追跡データのみを収集する
- 消去の権利 — お客様の追跡履歴を削除できるようにする
- 目的の制限 — 追跡データを配達追跡以外の目的に使用しない
- データ保持期間 — 古い追跡データの自動削除ポリシーを設定する
// Automatic data cleanup
async function cleanupOldTrackingData() {
const retentionPeriod = 90; // days
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - retentionPeriod);
await db.trackingEvents.deleteMany({
createdAt: { $lt: cutoff },
});
}
越境データに関する考慮事項
追跡データは本質的に国境を越えるものです。以下の規制に注意しましょう。
- 韓国のPIPA — 個人情報保護法(韓国の配送データに適用)
- 日本のAPPI — 個人情報の保護に関する法律
- 中国のPIPL — 個人情報保護法
WhereParcelはこれらの規制に準拠してデータを処理していますが、お客様のアプリケーションでもデータを適切に取り扱う必要があります。
セキュリティチェックリスト
追跡連携の構築時に、以下のチェックリストを活用してください。
- APIキーがコードではなく環境変数に保存されている
- APIコールがクライアントからではなくバックエンド経由で行われている
- 追跡エンドポイントにレート制限が設定されている
- 追跡検索に認証が必要とされている
- 送り状番号がログに全桁記録されていない
- Webhook署名が検証されている
- すべてのエンドポイントでHTTPSが適用されている
- データ保持ポリシーが実装されている
- 古い追跡データが自動的に削除されている
- APIキーのローテーションスケジュールが定義されている
- 列挙攻撃の検出が導入されている
まとめ
配送連携におけるセキュリティは、いくつかの核心的な原則に帰着します。APIキーを保護すること、追跡データへのアクセスを制御すること、Webhookの送信元を検証すること、そしてデータプライバシー規制を遵守することです。これらの対策は、ビジネスとお客様の機密性の高い配送情報の両方を守ります。
API認証の詳細については、はじめにガイドをご覧ください。Webhookのセキュリティについては、Webhook ベストプラクティスをご確認ください。