本番利用で重要AdCP は金銭を伴い、機微なキャンペーンデータを扱う可能性があります。実際の広告予算を扱う実装では、本書のセキュリティ対策を必ず実装してください。
AdCP は次のような高リスク環境で動作します:
- 金銭取引: 実際の広告費が動く
- 複数主体の信頼: 主体・パブリッシャー・オーケストレーター間の連携が必要
- 機微なデータ: 1P シグナル、未公開クリエイティブ、競合ターゲティング戦略
- 非同期オペレーション: 複数システム・プロトコルにまたがる
リスク分類
高リスク(金銭)
実際の広告予算をコミットするオペレーション:
| Operation | Risk | Primary Threat |
|---|
create_media_buy | Creates financial commitments | Budget fraud, credential theft |
update_media_buy | Modifies budgets and campaign parameters | Unauthorized modifications |
要件:
- 短命な認証情報(15 分以内で失効するトークン)
- トランザクション整合性のための署名
- 大規模予算向けの多要素認証または承認フロー
- 改ざん不可能なログによる完全な監査証跡
中リスク(データアクセス)
機微なビジネスデータへアクセスするオペレーション:
| Operation | Risk |
|---|
get_media_buy_delivery | Exposes performance metrics and spend data |
list_creatives | Access to creative assets |
sync_creatives | Uploads potentially sensitive creative content |
低リスク(ディスカバリー)
公開可能なオペレーション:
| Operation | Risk |
|---|
get_adcp_capabilities | Agent capability discovery |
get_products | Public inventory discovery |
list_creative_formats | Public format catalog |
Webhook Security
Signature Verification
Always verify webhook authenticity:
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expectedSignature}`)
);
}
app.post('/webhooks/adcp', (req, res) => {
const signature = req.headers['x-adcp-signature'];
const payload = JSON.stringify(req.body);
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook...
});
Replay Attack Prevention
Use timestamps and event IDs to prevent replay attacks:
async function isReplayAttack(timestamp, eventId) {
const eventTime = new Date(timestamp);
const now = new Date();
const fiveMinutes = 5 * 60 * 1000;
// Reject events older than 5 minutes
if (now - eventTime > fiveMinutes) {
console.warn(`Rejecting old webhook event ${eventId}`);
return true;
}
// Check if we've seen this event ID before
const seen = await db.hasSeenWebhookEvent(eventId);
if (seen) {
console.warn(`Rejecting duplicate webhook event ${eventId}`);
return true;
}
// Record this event ID
await db.recordWebhookEvent(eventId, timestamp);
return false;
}
Authentication Best Practices
Credential Storage
// Use secure key management systems
// Never commit credentials to version control
// Use environment variables or secret managers
// Example: Secure credential retrieval
async function getCredentials(principalId) {
// Retrieve from secure storage (AWS KMS, Vault, etc.)
const encrypted = await secretManager.get(`principal/${principalId}/apiKey`);
return decrypt(encrypted);
}
Token Expiration
Use short-lived tokens for high-risk operations:
const TOKEN_LIFETIMES = {
discovery: 3600, // 1 hour for read operations
financial: 900, // 15 minutes for financial operations
refresh: 86400 // 24 hours for refresh tokens
};
function validateToken(token, operationType) {
const decoded = jwt.verify(token, secret);
const maxAge = TOKEN_LIFETIMES[operationType] || TOKEN_LIFETIMES.discovery;
if (Date.now() - decoded.iat > maxAge * 1000) {
throw new Error('Token expired for this operation type');
}
return decoded;
}
Principal Isolation
Multi-Tenant Security
Orchestrators managing multiple principals must enforce strict isolation:
// ALWAYS filter by principal_id - never query without it
async function getMediaBuy(mediaBuyId, principalId) {
const mediaBuy = await db.mediaBuys.findOne({
id: mediaBuyId,
principal_id: principalId // Critical: prevents cross-principal access
});
if (!mediaBuy) {
// Generic error - don't reveal if campaign exists for another principal
throw new NotFoundError("Media buy not found");
}
return mediaBuy;
}
Row-Level Security
Implement row-level security in your database:
-- PostgreSQL example
CREATE POLICY principal_isolation ON media_buys
USING (principal_id = current_setting('app.current_principal')::uuid);
ALTER TABLE media_buys ENABLE ROW LEVEL SECURITY;
Financial Transaction Safety
Idempotency
All state-changing operations must support idempotency:
async function createMediaBuy(request) {
const { idempotency_key } = request;
// Check if this idempotency key was already processed
const existing = await db.findByIdempotencyKey(idempotency_key);
if (existing) {
// Return existing result, don't charge again
return existing;
}
// Process new request atomically
return db.transaction(async (tx) => {
const result = await processMediaBuy(tx, request);
await tx.idempotencyKeys.insert({ key: idempotency_key, result });
return result;
});
}
予算バリデーション
コミット前に予算を検証します:
async function validateBudget(request, principal) {
const { budget } = request;
// Check positive amount
if (budget.amount <= 0) {
throw new ValidationError('Budget must be positive');
}
// Check against account limits
const limits = await getAccountLimits(principal.id);
if (budget.amount > limits.daily_spend_limit) {
throw new BudgetError('Exceeds daily spend limit');
}
// Check available balance
const balance = await getAvailableBalance(principal.id);
if (budget.amount > balance) {
throw new BudgetError('Insufficient balance');
}
}
トランスポートセキュリティ
HTTPS 要件
- すべての AdCP 通信で HTTPS (TLS 1.3+、最低 TLS 1.2) を使用
- SSL 証明書を検証(本番で自己署名は不可)
- HTTP Strict Transport Security (HSTS) ヘッダーを設定
app.use((req, res, next) => {
// HSTS header
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Prevent MIME sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});
入力バリデーション
リクエストバリデーション
ユーザー入力はすべて検証します:
const INPUT_LIMITS = {
targeting_brief_max_length: 5000,
creative_upload_max_size: 100 * 1024 * 1024, // 100MB
max_formats_per_request: 50,
max_products_per_query: 100
};
function validateRequest(request) {
// Check string lengths
if (request.brief?.length > INPUT_LIMITS.targeting_brief_max_length) {
throw new ValidationError('Brief exceeds maximum length');
}
// Validate IDs are proper UUIDs
if (request.product_id && !isValidUUID(request.product_id)) {
throw new ValidationError('Invalid product_id format');
}
// Reject unexpected fields
const allowedFields = ['brief', 'product_id', 'budget', 'context_id'];
for (const field of Object.keys(request)) {
if (!allowedFields.includes(field)) {
throw new ValidationError(`Unexpected field: ${field}`);
}
}
}
SQL インジェクション防止
常にパラメタライズドクエリを使用します:
// GOOD: Parameterized query
const result = await db.query(
'SELECT * FROM media_buys WHERE id = $1 AND principal_id = $2',
[mediaBuyId, principalId]
);
// BAD: String concatenation (NEVER do this)
// const result = await db.query(
// `SELECT * FROM media_buys WHERE id = '${mediaBuyId}'`
// );
監査ログ
ログに必須のイベント
セキュリティ関連イベントをすべて記録します:
const LOG_EVENTS = {
AUTH_SUCCESS: 'auth_success',
AUTH_FAILURE: 'auth_failure',
BUDGET_COMMIT: 'budget_commit',
BUDGET_MODIFY: 'budget_modify',
ACCESS_DENIED: 'access_denied',
WEBHOOK_VERIFIED: 'webhook_verified',
WEBHOOK_REJECTED: 'webhook_rejected'
};
function logSecurityEvent(eventType, details) {
console.log(JSON.stringify({
event: eventType,
timestamp: new Date().toISOString(),
principal_id: details.principalId,
ip_address: details.ipAddress,
resource: details.resource,
outcome: details.outcome,
// NEVER log: credentials, PII, targeting briefs
}));
}
ログ保存期間
- セキュリティログ: 最低 90 日(推奨 365 日)
- 財務ログ: 7 年(コンプライアンス要件)
- アクセスログ: 最低 30 日
セキュリティチェックリスト
パブリッシャー(AdCP サーバー)
プリンシパル(AdCP クライアント)
オーケストレーター(マルチプリンシパルエージェント)
次のステップ