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

概要

AdCP は次のような高リスク環境で動作する:
  • 金銭取引: 実際の広告費が動く
  • 複数主体の信頼: Principal・パブリッシャー・オーケストレーター間の連携が必要
  • 機微なデータ: 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

Webhook の署名検証とリプレイ防止は Push Notifications で定義されています。規範的な要件:
  • アルゴリズム: HMAC-SHA256 のみ
  • 署名対象メッセージ: {unix_timestamp}.{raw_http_body_bytes} — JSON を再シリアライズしてはなりません
  • タイミングセーフ比較: 定数時間比較を使用しなければなりません(例: timingSafeEqual
  • リプレイウィンドウ: |current_time - timestamp| > 300 秒のリクエストは拒否します
  • シークレットの最小長: 32 バイト

検証順序

無効なリクエストの計算コストを最小化するため、この順序で検証する:
  1. X-ADCP-Signature または X-ADCP-Timestamp ヘッダーが欠けている場合は拒否します
  2. タイムスタンプが数値でない場合は拒否します
  3. タイムスタンプが 5 分のウィンドウ外の場合は拒否します
  4. HMAC を計算して比較します

シークレットローテーション

  • 受信側はローテーション中に現在と前のシークレット両方からの署名を受け入れなければなりません
  • ローテーションウィンドウはリプレイウィンドウ(5 分)を超えるべきではありません
  • パブリッシャーはローテーション後すぐに新しいシークレットで署名を開始します

Webhook URL バリデーション(SSRF)

バイヤーが提供する push_notification_config.url は SSRF のベクターとなります。パブリッシャーはしなければなりません:
  • HTTPS 以外の URL を拒否します
  • プライベート/予約済み IP レンジ(RFC 1918、RFC 6598、リンクローカル)をターゲットにする URL を拒否します
  • 接続前に DNS を解決して、解決された IP がプライベートでないことを検証します

認証のベストプラクティス

認証情報の保管

// 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);
}

トークンの有効期限

高リスクオペレーションには短命なトークンを使用します:
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

マルチテナントセキュリティ

複数の Principal を管理するオーケストレーターは厳格な分離を強制しなければなりません:
// 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;
}

行レベルセキュリティ

データベースに行レベルセキュリティを実装します:
-- 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;

金融トランザクションの安全性

冪等性

状態を変更するすべてのオペレーションが冪等性をサポートしなければなりません:
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;
  });
}
バイヤーは (seller, request) ペアごとに一意の idempotency_key を生成しなければなりません。同じキーを複数のセラーに再利用すると、共謀するセラーが同一バイヤーのリクエストを相関づけることができます。リクエストごとに新しい UUID v4 を使用すること。

Governance Context

governance_context はガバナンスエージェントからバイヤー、セラー、そして戻りという信頼境界を越える不透明な文字列です。実装者は各ホップでこれを信頼されない入力として扱うべきだ:
  • ガバナンスエージェントgovernance_context をサーバーサイドの状態へのルックアップキーまたは署名済みトークン(例: HMAC-SHA256)として扱わなければなりません。値に平文の状態をエンコードすると、仲介者による改ざんを許す。状態を直接エンコードする場合は、変更を検出できるよう署名しなければなりません。
  • ガバナンスエージェントは各 governance_context を特定の (plan_id, media_buy_id) タプルに紐付け、リクエストの識別子と一致しないコンテキストを拒否すべきです。これにより、あるメディアバイのコンテキストを別のメディアバイにリプレイすることを防ぐ。
  • セラーgovernance_context を解釈または変更してはなりません。受け取ったまま保存し、その後のすべてのガバナンス呼び出しにそのまま含めます。
  • バイヤーはガバナンスエージェントから governance_context を受け取り、プロトコルエンベロープに添付します。値を構築または変更してはなりません。

予算バリデーション

コミット前に予算を検証する:
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 に行レベルセキュリティを導入します
  • 全操作をプリンシパル識別子付きでログに記録します
  • プリンシパルごとにレート制限を実装します

次のステップ