Skip to main content
Critical for Production UseAdCP handles financial commitments and potentially sensitive campaign data. Implementations managing real advertising budgets must implement the security controls outlined in this document.

Overview

AdCP operates in a high-stakes environment where:
  • Financial transactions involve real advertising spend
  • Multi-party trust requires coordination between Principals, Publishers, and Orchestrators
  • Sensitive data includes first-party signals, pre-launch creatives, and competitive targeting strategies
  • Asynchronous operations span multiple systems and protocols

Risk Classification

High-Risk Operations (Financial)

These operations commit real advertising budgets:
OperationRiskPrimary Threat
create_media_buyCreates financial commitmentsBudget fraud, credential theft
update_media_buyModifies budgets and campaign parametersUnauthorized modifications
Requirements:
  • Short-lived credentials (tokens expiring in ≤15 minutes)
  • Request signing for transaction integrity
  • Multi-factor authentication or approval workflows for large budgets
  • Full audit trail with immutable logging

Medium-Risk Operations (Data Access)

These operations access sensitive business data:
OperationRisk
get_media_buy_deliveryExposes performance metrics and spend data
list_creativesAccess to creative assets
sync_creativesUploads potentially sensitive creative content

Low-Risk Operations (Discovery)

These operations are publicly accessible:
OperationRisk
get_adcp_capabilitiesAgent capability discovery
get_productsPublic inventory discovery
list_creative_formatsPublic format catalog

Webhook Security

Webhook signature verification and replay prevention are defined in Push Notifications. The normative requirements:
  • Algorithm: HMAC-SHA256 only
  • Signed message: {unix_timestamp}.{raw_http_body_bytes} — never re-serialize the JSON
  • Timing-safe comparison: MUST use constant-time comparison (e.g., timingSafeEqual)
  • Replay window: Reject requests where |current_time - timestamp| > 300 seconds
  • Minimum secret length: 32 bytes

Verification order

Verify in this order to minimize computation on invalid requests:
  1. Reject if X-ADCP-Signature or X-ADCP-Timestamp header is missing
  2. Reject if timestamp is non-numeric
  3. Reject if timestamp is outside the 5-minute window
  4. Compute and compare HMAC

Secret rotation

  • Receivers MUST accept signatures from both current and previous secret during rotation
  • Rotation window SHOULD NOT exceed the replay window (5 minutes)
  • Publishers begin signing with the new secret immediately upon rotation

Webhook URL validation (SSRF)

Buyer-provided push_notification_config.url is an SSRF vector. Publishers MUST:
  • Reject non-HTTPS URLs
  • Reject URLs targeting private/reserved IP ranges (RFC 1918, RFC 6598, link-local)
  • Resolve DNS and validate the resolved IP is not private before connecting

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;
  });
}
Buyers MUST generate a unique idempotency_key per (seller, request) pair. Reusing the same key across sellers allows colluding sellers to correlate requests from the same buyer. Use a fresh UUID v4 for each request.

Governance Context

governance_context is an opaque string that crosses trust boundaries — from governance agent to buyer to seller and back. Implementers should treat it as untrusted input at each hop:
  • Governance agents MUST treat governance_context as a lookup key into server-side state or a signed token (e.g., HMAC-SHA256). Encoding plain-text state in the value allows tampering by intermediaries. If state is encoded directly, it MUST be signed so modification is detectable.
  • Governance agents SHOULD bind each governance_context to a specific (plan_id, media_buy_id) tuple and reject contexts that don’t match the request’s identifiers. This prevents replay of a context from one media buy onto another.
  • Sellers MUST NOT interpret or modify governance_context. Store it as received and include it verbatim on all subsequent governance calls.
  • Buyers receive governance_context from the governance agent and attach it to the protocol envelope. They MUST NOT construct or modify the value.

Budget Validation

Validate budgets before committing:
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');
  }
}

Transport Security

HTTPS Requirements

  • All AdCP communications must use HTTPS with TLS 1.3+ (TLS 1.2 minimum)
  • Validate SSL certificates (no self-signed certificates in production)
  • Implement HTTP Strict Transport Security (HSTS) headers
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();
});

Input Validation

Request Validation

Validate all user-provided input:
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 Injection Prevention

Always use parameterized queries:
// 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}'`
// );

Audit Logging

Required Log Events

Log all security-relevant events:
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
  }));
}

Log Retention

  • Security logs: 90 days minimum (365 days recommended)
  • Financial logs: 7 years (compliance requirement)
  • Access logs: 30 days minimum

Security Checklist

For Publishers (AdCP Servers)

  • Implement strong authentication (OAuth 2.0, API keys, or mTLS)
  • Enforce principal isolation in all database queries
  • Implement idempotency for financial operations
  • Validate all input with strict schema validation
  • Use TLS 1.3+ for all communications
  • Verify webhook signatures cryptographically
  • Log all security events immutably

For Principals (AdCP Clients)

  • Store credentials in secure key management system
  • Rotate credentials every 90 days
  • Use HTTPS for all AdCP communications
  • Validate responses from publishers
  • Implement alerts for unusual spending patterns

For Orchestrators (Multi-Principal Agents)

  • Store each principal’s credentials separately (encrypted)
  • Enforce principal_id filtering in ALL queries
  • Use row-level security in databases
  • Log all operations with principal identity
  • Implement per-principal rate limiting

Next Steps