Skip to main content
本番利用で重要AdCP は金銭を伴い、機微なキャンペーンデータを扱う可能性があります。実際の広告予算を扱う実装では、本書のセキュリティ対策を必ず実装してください。

概要

AdCP は次のような高リスク環境で動作します:
  • 金銭取引: 実際の広告費が動く
  • 複数主体の信頼: 主体・パブリッシャー・オーケストレーター間の連携が必要
  • 機微なデータ: 1P シグナル、未公開クリエイティブ、競合ターゲティング戦略
  • 非同期オペレーション: 複数システム・プロトコルにまたがる

リスク分類

高リスク(金銭)

実際の広告予算をコミットするオペレーション:
OperationRiskPrimary Threat
create_media_buyCreates financial commitmentsBudget fraud, credential theft
update_media_buyModifies budgets and campaign parametersUnauthorized modifications
要件:
  • 短命な認証情報(15 分以内で失効するトークン)
  • トランザクション整合性のための署名
  • 大規模予算向けの多要素認証または承認フロー
  • 改ざん不可能なログによる完全な監査証跡

中リスク(データアクセス)

機微なビジネスデータへアクセスするオペレーション:
OperationRisk
get_media_buy_deliveryExposes performance metrics and spend data
list_creativesAccess to creative assets
sync_creativesUploads potentially sensitive creative content

低リスク(ディスカバリー)

公開可能なオペレーション:
OperationRisk
get_adcp_capabilitiesAgent capability discovery
get_productsPublic inventory discovery
list_creative_formatsPublic 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 サーバー)

  • 強固な認証を実装(OAuth 2.0, API キー, mTLS など)
  • すべての DB クエリでプリンシパル分離を徹底
  • 金銭処理は冪等性を担保
  • 厳密なスキーマバリデーションで入力を検証
  • すべての通信で TLS 1.3+ を使用
  • Webhook 署名を暗号学的に検証
  • セキュリティイベントを改ざん不可で記録

プリンシパル(AdCP クライアント)

  • 認証情報を安全なキーマネージャに保管
  • 90 日ごとに認証情報をローテーション
  • すべての AdCP 通信で HTTPS を使用
  • パブリッシャーからのレスポンスを検証
  • 異常な支出パターンにアラートを設定

オーケストレーター(マルチプリンシパルエージェント)

  • プリンシパルごとに認証情報を分離保管(暗号化)
  • すべてのクエリで principal_id フィルタを適用
  • DB に行レベルセキュリティを導入
  • 全操作をプリンシパル識別子付きでログ
  • プリンシパルごとにレート制限を実装

次のステップ