Safeguard
Application Security

The Migration Is the Riskiest Part of the Pull Request

It runs in production with the highest privileges in your system, usually unattended, and gets reviewed as an implementation detail at the bottom of the diff. It can drop a constraint that was the only thing enforcing a security property.

Shadab Khan
Engineering
6 min read

A database migration is code that runs in production, with the highest privileges in your system, usually without a human watching, and it gets reviewed as an implementation detail at the bottom of a pull request.

It can drop a table, remove a constraint that was enforcing a security property, silently truncate a column, or expose data through a view. Most review attention goes to the application code in the same pull request, which can do considerably less damage.

This post is what to look at in a migration specifically. For whoever reviews them, which is usually whoever is next in the rotation.

The privileges involved

Application code runs as a user with select, insert, update and delete on specific tables. A migration runs as a user that can alter schema, drop objects, and change permissions. In many deployments it is the same credential that owns every object in the database.

That credential usually lives in your CI environment, because migrations run during deployment. So the blast radius of a compromised pipeline includes schema-level access to production data, and the blast radius of a bad migration includes anything that credential can reach.

Two practical consequences. The migration credential should be separate from the application credential and should not be usable to read bulk data. And migrations should be the only thing that uses it, so its use is a signal.

What to look for in review

Constraints being removed. A NOT NULL, a foreign key, a unique index or a check constraint is frequently the only thing enforcing an invariant the application assumes. Dropping one to make a migration pass is common and the reasoning rarely reaches the pull request.

The security-relevant cases: a unique constraint on an email or username preventing account collision, a foreign key preventing orphaned records that queries then mis-scope, and a check constraint enforcing a tenant relationship.

Column type changes that truncate. Narrowing a column can silently drop data, and some engines will do it without complaint depending on mode. If the column holds a token, a hash or an identifier, truncation is a correctness and sometimes a security problem.

New columns with a default that backfills. On a large table this rewrites every row and can lock it for a long time. The availability consequence is real, and the usual fix, running it outside the transaction, means a partial failure leaves the schema half-migrated.

Views and grants. A view created for a report may expose columns the application layer deliberately never returns. A GRANT in a migration changes who can read what, and it is the single line most likely to be skimmed.

Data migrations mixed with schema migrations. An UPDATE touching every row is a data change wearing a schema change's clothes. It usually has no review of what it does to records it was not written for, and it is invisible to the change management process you documented, because that process describes deploys.

Irreversible operations. A dropped column cannot be undone by re-adding it. If the down migration is a lie, say so explicitly rather than writing something that appears to reverse it.

The review questions

Five, and they take a few minutes:

  1. What happens if this fails halfway? Is the migration transactional, and if not, what is the state after a partial failure.
  2. Is it reversible, genuinely? If not, is that acknowledged, and is there a backup taken immediately before.
  3. Does it lock anything, and for how long on production-sized data? Row counts matter here; a migration tested on a thousand rows tells you nothing about ten million.
  4. Does it remove a constraint, and what was that constraint protecting?
  5. Does it change who can see what? Grants, views, row-level security policies.

The ordering problem with deploys

Migrations and application deploys race, and the ordering determines whether you have downtime or a broken state.

The general rule is that schema changes must be compatible with both the old and new application code, because for a period both are running. That means the expand-and-contract pattern: add the new column, deploy code writing to both, backfill, deploy code reading the new one, then remove the old column in a later migration.

Teams skip the intermediate steps under time pressure, and the failure is an outage rather than a security problem. It is worth knowing because the pressure to skip steps is where the constraint-dropping and the untested UPDATE come from.

Keep them as audit evidence

Migrations are part of your change record, and they are usually better evidence than anything else you have about how the data layer changed.

Keep them in version control, applied in order, with the applied-at timestamp recorded in the database. That table is a chronological record of every structural change, who authored it and when it ran. Auditors like it, and it is free because the migration tool already maintains it.

What is not covered is the manual change: someone connecting to production and running DDL by hand. That leaves no row in the migrations table and no trace in your pipeline. If your database allows interactive DDL from a human session, that is a gap in your change management regardless of how good your migrations are, and the fix is either to remove that access or to log and review it.

The concession

Reviewing every migration with this level of care is disproportionate for most of them. Adding a nullable column to a small table is not a security event and treating it as one will exhaust the reviewer before they reach the migration that matters.

So triage on three signals: does it drop or alter anything that exists, does it touch data rather than structure, and does it change grants or views. A migration that only adds new nullable objects can be skimmed. Everything else deserves the five questions, and that is a small minority of the migrations most teams write.

The implication

The migration is the part of the pull request with the most authority and the least scrutiny, and the reason is ordinary: it looks like configuration, it is written in an unfamiliar dialect, and the reviewer's attention was spent on the application code above it.

Read the migration first, then the code. It is a two-word change to your review habit and it inverts the attention to match the risk.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.