택배 추적 보안: 배송 데이터와 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 모범 사례를 참고하세요.