Skip to content

Security: DanielTomaro13/ads-manager

Security

docs/SECURITY.md

Security

Threat model

Threat Mitigation
DB dump exposes customer ad-platform tokens Envelope encryption; attacker needs DB and KMS.
Compromised user account → unlimited ad spend Hard budget caps per connection; step-up auth for high-value mutations; anomaly alerts.
Malicious tenant reads another tenant's data Postgres RLS enforced at DB layer; every query sets app.current_org.
OAuth callback CSRF state parameter signed + single-use; PKCE for all flows.
Leaked refresh token used externally Bind tokens to organization_id; rotate on suspicious use; IP allowlist optional.
Worker runs malicious rule payload Rule DSL is declarative (not code-exec); bounded action set.
XSS pastes ad copy with script tags Sanitise + render as text in previews; CSP on frontend.

Token storage — envelope encryption

plaintext_token
      │
      ▼
AES-GCM (per-org DEK)  ──►  ciphertext stored in `connections.access_token_enc`
      ▲
      │
    DEK (32 bytes, random, per-org)
      │
      ▼
KMS KEK (Azure Key Vault / AWS KMS)  ──►  wrapped DEK stored in `organizations.wrapped_dek`
  • To read a token the API: (1) fetches the wrapped DEK, (2) asks KMS to unwrap, (3) decrypts the ciphertext. DEK is held in memory for the request only.
  • Key rotation: generate new DEK, re-encrypt all tokens for the org, update wrapped_dek. Can be done live.
  • Local dev falls back to a static Fernet key from env (clearly labelled as insecure) so engineers don't need KMS to run the app.

Implementation lives in apps/api/app/core/encryption.py.

Row-level security

Every tenant table:

ALTER TABLE campaigns ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON campaigns
  USING (organization_id = current_setting('app.current_org')::uuid);

Middleware sets the GUC at the start of each request after auth. Background jobs set it explicitly before each org's work. Integration tests assert that cross-tenant queries return zero rows.

Audit log

Every write (API mutation, rule action, admin action) records:

  • actor_type: user | rule | system | admin
  • actor_id
  • organization_id
  • action: e.g. campaign.create, connection.revoke, rule.execute
  • target_type + target_id
  • diff: JSONB before/after
  • ip, user_agent, request_id
  • ts

Audit log is append-only (no UPDATE/DELETE permission for app role).

Rate limiting

  • Per-IP on auth endpoints (5/min for login).
  • Per-org on platform-write endpoints (prevents runaway costs from bugs).
  • Global per-platform SDK rate limits handled in connector with token-bucket + retry on RESOURCE_EXHAUSTED.

Secrets management

  • Never commit credentials. .env.example is the only tracked env file.
  • Production: Azure Key Vault / AWS Secrets Manager. Pulled at container start into env.
  • pre-commit hook runs detect-secrets to block accidental commits.

Dependency hygiene

  • pip-audit + npm audit run in CI.
  • Dependabot / Renovate for PR auto-updates.
  • Pin exact versions in lock files.

Data deletion

On account deletion we (a) revoke OAuth at each platform, (b) delete tokens, (c) anonymise audit-log actor fields but retain events (30-day grace, then hard delete). Export bundle offered before deletion.

There aren’t any published security advisories