Migration Runner Privilege

Database migration runners require privileged database credentials β€” at minimum, the ability to ALTER TABLE, CREATE INDEX, and DROP COLUMN. In practice, many migration setups use the application's superuser or owner account, which can CREATE and DROP tables, CREATE DATABASE, and sometimes GRANT permissions to other users. This is the broadest reasonable database permission set, and it is exercised automatically during every deployment.

The migration runner itself is a trusted component β€” CI/CD pipelines typically run migrations without human approval as part of the automated deployment process. This trust is justified by the assumption that migration files are reviewed and correct. When that assumption fails β€” through a supply chain attack on migration tooling, a compromised CI pipeline, or a malicious migration file committed through a social engineering attack β€” the migration runner executes arbitrary SQL with its full permission set.

The schema owner problem: In PostgreSQL, creating a schema requires a database role that owns that schema. Many Flyway and Atlas setups run as the database owner role, which can create new roles, grant permissions, and modify row-level security policies β€” not just schema objects. A single malicious migration can permanently backdoor database access.

Schema Poisoning Attacks

Schema poisoning refers to injecting malicious database objects β€” functions, triggers, stored procedures, or views β€” through the migration process. Unlike application code vulnerabilities that require runtime exploitation, schema objects persist in the database and execute server-side with database-level permissions. Common schema poisoning patterns include:

  • Backdoor triggers: A migration creates an AFTER INSERT trigger on the users table that exfiltrates new user credentials to an attacker-controlled external table or via pg_notify. Triggers are invisible to application-level logging and persist until explicitly dropped.
  • Malicious stored procedures: Overriding a legitimate stored procedure name with a version that performs the original function plus exfiltrates data or grants additional permissions.
  • View-based data exposure: Creating views that aggregate sensitive data across tables and granting public access, creating a persistent data exposure path invisible to table-level permission audits.
-- Example of a schema poisoning migration (what an attacker would inject) -- V2026.04.13__add_user_activity_index.sql (looks like a legitimate migration) -- Legitimate change: CREATE INDEX idx_users_email ON users(email); -- Hidden malicious trigger: CREATE OR REPLACE FUNCTION exfiltrate_creds() RETURNS TRIGGER AS $$ BEGIN INSERT INTO audit_shadow_table(email, pwd_hash, ts) VALUES (NEW.email, NEW.password_hash, now()); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER shadow_insert AFTER INSERT ON users FOR EACH ROW EXECUTE FUNCTION exfiltrate_creds();

Migration History Table Tampering

Flyway uses a flyway_schema_history table to track which migrations have been applied. Atlas and Liquibase use similar tracking tables. An attacker with migration runner credentials can modify these tracking tables to mark migrations as applied (preventing legitimate migrations from running) or as not applied (causing already-applied migrations to re-run). Re-running a data-destructive migration β€” a legitimate one that drops a temporary table or clears a cache β€” by clearing its history entry can cause significant data loss while appearing as a tool malfunction rather than an attack.

Flyway's checksum validation: Flyway detects modifications to already-applied migration files through checksums stored in the history table. But if the history table itself is compromised, the checksum records can be updated to match a modified migration β€” bypassing the integrity check entirely.

Rollback Attack Patterns

Many migration tools support explicit rollback scripts (Flyway Teams, Liquibase rollback). Rollback scripts that reverse a migration are applied in incident response or deployment failure scenarios β€” often under time pressure with reduced review. An attacker who can modify a rollback script can inject destructive SQL that is applied precisely when the team is in a stressed incident response mode, when careful review is least likely.

Atlas's declarative migration model β€” where you define desired state and Atlas generates the migration β€” creates a different risk: if the desired state file is compromised, the generated migration may include destructive changes (dropping columns, removing indexes, altering constraints) that are presented as automated schema drift corrections rather than explicit adversarial changes.

CI Migration Credentials

Migration runner credentials stored in CI secrets represent a persistent, high-privilege database access path. A CI secret exposure through log leakage, PR comment injection, or a compromised CI provider gives an attacker direct DDL access to production databases. Unlike application service accounts that typically have DML (SELECT, INSERT, UPDATE, DELETE) permissions, migration runner credentials often have DDL (CREATE, DROP, ALTER) permissions β€” a significantly higher blast radius.

Separate DDL and DML credentials: Run migrations with a dedicated migration role that has DDL permissions on schema objects but no access to application data. The application service account should have DML permissions only. A compromised application service account cannot modify schema; a compromised migration credential cannot read application data.

Securing the Migration Pipeline

  1. Require mandatory review for all migration files: Apply branch protection rules that require at least one security-aware reviewer for any changes to migration directories. Migration files are privileged SQL; treat them like production code with database admin rights.
  2. Use separate migration roles with minimal DDL scope: Create a dedicated database role for the migration runner with permissions limited to the specific schema objects it manages. Do not use the superuser, owner, or application role for migrations.
  3. Validate migration content with automated SQL linting: Apply SQL linting tools (SQLFluff, pganalyze) that flag dangerous patterns β€” CREATE TRIGGER on sensitive tables, GRANT statements, stored procedure creation β€” as review blockers in CI.
  4. Protect migration history tables: Grant the migration runner role CRUD access to the history table, but grant the application role only read access. Preventing the application from modifying migration history eliminates one class of history tampering attack.
  5. Require dry-run output review for production migrations: Run migrations with a preview or dry-run flag in CI and store the output as a deployment artifact. Reviewers approve the dry-run output (the actual SQL that will execute) rather than just the migration file.
  6. Alert on unexpected migration executions: Monitor migration table modifications. An unexpected row insertion into flyway_schema_history outside a known deployment window is a strong indicator of unauthorised migration execution.