To review a database migration for security impact, read the change script itself, not just the application diff, and check it against a short list: new sensitive columns without encryption, credential columns stored without hashing, grants wider than the job needs, new privileged accounts, audit triggers or logging that disappear, and destructive or unbounded statements. Record who reviewed it and what they decided, because that record is what an auditor asks for later. Safeguard's DB Change-logs integration (GA) runs the same review over uploaded change scripts in SQL, Liquibase, Flyway, Alembic and ORM formats and maps each finding to a CWE and to the compliance controls it bears on.
Key takeaways
- A migration is a production change with the highest privilege in your release. The account that runs it can usually create tables, alter grants and drop data, so the script deserves the same review as code that touches authentication.
- Six checks catch most of the security impact in a schema change: sensitive columns without encryption, credential columns without hashing, over-broad grants, new privileged accounts, removed audit triggers, and destructive or unbounded statements.
- NIST SP 800-53 asks for security impact analysis before a change (CM-4) and controlled, recorded change (CM-3). HIPAA's technical safeguards cover audit controls and encryption of ePHI. A migration that drops an audit trigger can break both at once.
- Every migration tool already keeps a record of what ran (Flyway's schema history table, Liquibase's DATABASECHANGELOG table, Alembic's alembic_version table, Django's record of applied migrations). That table tells you what ran. It does not tell you whether anyone looked at it for security.
- Safeguard capabilities in this post, all GA: the DB Change-logs integration for uploaded change scripts, policies and gates (actions Block, Warn and Notify, with approval through exceptions), the Safeguard CLI and CI scan, and autonomous remediation, which ships switched off with every category set to Manual.
- What stays yours: deciding whether a change is acceptable, running the migration, testing it against real data volumes, and owning the live database's configuration.
The situation
The trigger is usually small. A pull request lands late on a Thursday with a feature and a migration folder. The feature diff gets three comments. The migration gets a thumbs up, because it is "just a schema change". It adds a date_of_birth and a tax_id column to customers as plain text, grants ALL on the table to a reporting role so the analytics job stops failing, and drops a trigger that was "slowing down bulk loads". The trigger was the one that wrote every update on customers to an audit table.
Nobody did anything malicious. Each line had a reason. But after that migration runs, regulated data sits unencrypted in a column, a reporting role can delete rows, and changes to customer records stop leaving a trail. The first person to notice is often an auditor, months later, asking for evidence of who changed a customer record in March. The answer is that nobody can tell, because the audit trigger went away in February.
Who asks, and by when, depends on your vertical:
- In a US fintech or bank technology team, the ask comes from the security or risk function preparing for an examination or a SOC 2 audit window. They want evidence that database changes followed the change management process and got a security look.
- In a health technology team, it comes from the HIPAA security officer, who needs to show that audit controls on ePHI were in place for the whole period and that ePHI at rest is protected.
- In any team selling to enterprises, it comes from a customer security questionnaire: "Describe how database schema changes are reviewed for security impact before deployment."
A bad week looks like this. On Monday someone finds the plaintext column. On Tuesday you learn it has been in production for six weeks and replicated to two read replicas and a nightly analytics export. On Wednesday you write a migration to encrypt it, and discover the backfill will lock the table for longer than your maintenance window. On Thursday the auditor asks for the review record of the original migration. On Friday you are writing a remediation plan instead of shipping.
The fix is not heroic. It is a short, repeatable review of the change script, done before merge, with a record. The rest of this post is how to do that with whatever tools you have, then how Safeguard does it.
Why migrations escape review
Migrations get less scrutiny than application code for reasons that are easy to understand and worth naming, because each one suggests a fix.
They look mechanical. Many migrations are generated. Django's makemigrations writes Python operations from your model changes, and Alembic's autogenerate compares your models to the database and writes a draft revision. Reviewers see generated files and assume the generator is responsible. The generator only knows what you told the model. It has no idea that tax_id is regulated data.
The SQL is not in the diff. With ORM migrations, the pull request shows Python or another host language, not the SQL that will run. Django's sqlmigrate command prints the SQL for a migration, and Alembic can generate SQL scripts in offline mode, but someone has to run them. If the reviewer never sees the statements, they cannot judge the grants or the DELETE.
They run with the most privilege. The account that applies migrations usually owns the schema. It can create roles, change grants and drop tables. Application code runs as a less privileged user, if you have separated the roles. So a migration can do things the application never could, and a mistake in it has a wider reach.
History tables record execution, not judgement. Flyway records each applied migration in its schema history table. Liquibase records each changeset in its DATABASECHANGELOG table, identified by a combination of its id, author and file name. Alembic stores the current revision in its alembic_version table, and Django keeps its own record of which migrations have been applied. Those records are useful. They prove what ran and when. They say nothing about whether the change was safe or who decided it was.
Rollback feels like a safety net. Teams relax because a migration "can be rolled back". Many cannot be reversed in any meaningful way. A dropped column's data is gone. A grant that was used for a week has already been used. An audit gap cannot be filled in later.
Ownership is split. Application engineers write migrations, platform or database engineers run them, and security reviews code. The script falls between all three.
The rules behind it
This section is a reading of public standards to show where migration review fits. It is not legal, regulatory or audit advice, and it does not describe what any specific examiner or auditor will ask for. Read the primary texts for your own obligations.
NIST SP 800-53: impact analysis and change control
NIST SP 800-53 Revision 5 is the control catalogue behind FedRAMP and many US federal and financial programmes, and its language turns up in SOC 2 and customer questionnaires too. Several controls apply directly to a schema change.
- CM-3, Configuration Change Control. Determine and document the types of changes that are configuration controlled, review proposed changes and approve or disapprove them with explicit consideration for security and privacy impact analyses, document the decisions, and retain records of the changes. A database schema and its grants are configuration. A migration is a configuration change.
- CM-4, Impact Analyses. Analyse changes to the system to determine potential security and privacy impacts before implementing them. This is the control that most directly describes "review the migration for security impact".
- AC-6, Least Privilege. Allow only authorised accesses that are necessary to accomplish assigned tasks. A
GRANT ALLto a reporting role fails this on its face. - AU-2 and AU-12, Event Logging and Audit Record Generation. Identify the events the system must be capable of logging and generate the records. An audit trigger is one way a database meets these. Dropping it is a change to your logging capability.
- SC-28, Protection of Information at Rest. Protect information at rest, with the organisation choosing whether the control covers confidentiality, integrity or both, and for which information. A new column holding sensitive data is where this control either holds or does not.
NIST SP 800-218, the Secure Software Development Framework
The SSDF describes practices for producing secure software. Two practice groups bear on migrations. Under "Produce Well-Secured Software" (PW), practice PW.7 is to review and analyse human-readable code to help identify vulnerabilities so they can be corrected before the software is released. A migration script is human-readable code that changes production. Under "Respond to Vulnerabilities" (RV), teams identify, analyse and remediate vulnerabilities on an ongoing basis. A plaintext sensitive column found after deployment is exactly that kind of finding.
The SSDF is a framework of outcomes, not a checklist of tools. It does not tell you to scan migrations. It tells you to review code that matters, and the migration folder qualifies.
HIPAA Security Rule technical safeguards
For teams handling electronic protected health information, 45 CFR 164.312 sets the technical safeguards. Two of them meet migrations head on. Section 164.312(b), audit controls, requires mechanisms that record and examine activity in systems that contain or use ePHI. Section 164.312(a)(2)(iv), encryption and decryption, is an addressable implementation specification for protecting ePHI. A migration that adds an ePHI column in plaintext, or drops the trigger that records changes to ePHI, is a change to how you meet those safeguards, and your risk analysis should see it.
CWE: naming the weakness
MITRE's Common Weakness Enumeration gives each class of weakness a stable identifier, which makes findings comparable across tools and auditors. The ones that come up most in schema changes are:
- CWE-311, Missing Encryption of Sensitive Data. The product does not encrypt sensitive or critical information before storage or transmission.
- CWE-256, Plaintext Storage of a Password, and CWE-916, Use of Password Hash With Insufficient Computational Effort. A credential column without hashing, or with a fast general purpose hash, sits here.
- CWE-269, Improper Privilege Management. New roles and broad grants.
- CWE-778, Insufficient Logging. A removed audit trigger, or a table holding sensitive data with no change record.
Using CWE identifiers in your review record is cheap and pays off when an auditor or customer asks how you classify findings.
OWASP guidance on database security and password storage
The OWASP Database Security Cheat Sheet recommends the principle of least privilege for database accounts, so each account has only the minimal permissions the application needs, and separate databases and accounts for development, test and production. The OWASP Password Storage Cheat Sheet recommends a slow, salted, purpose-built password hashing algorithm, and names Argon2id, scrypt, bcrypt and PBKDF2 as options. Both are good references to cite in a review comment when you push back on a migration.
What a migration looks like in each tool
You cannot review what you cannot find. Here is where the change actually lives in the common frameworks, from each tool's own documentation.
| Tool | Where the change lives | What records that it ran | How to see the SQL |
|---|---|---|---|
| Flyway | Versioned migrations, which carry a version and run once in order, and repeatable migrations; SQL and Java-based migrations are both supported | The schema history table | SQL migrations are already SQL; read the file |
| Liquibase | A changelog file containing changesets, written in SQL, XML, YAML or JSON | The DATABASECHANGELOG table, which identifies each changeset by its id, author and file name | SQL changelogs are SQL; for other formats, the update-sql command shows the SQL Liquibase will run |
| Alembic | Revision scripts in the versions directory, each with upgrade() and downgrade() functions | The alembic_version table | Offline mode generates a SQL script instead of running against a database |
| Django | Migration files per app, generated by makemigrations and applied by migrate | Django's record of applied migrations in the database | sqlmigrate prints the SQL for a named migration |
Three practical consequences follow.
First, the review unit is the file, and in Liquibase the changeset. Ask reviewers to comment on the change script, not on the feature.
Second, for ORM migrations, put the generated SQL in front of the reviewer. A CI job that runs sqlmigrate or Alembic's offline mode and posts the output to the pull request costs little and removes the "I never saw the SQL" gap.
Third, some operations hide SQL inside the framework. Django's RunSQL and RunPython operations, Alembic's op.execute, Liquibase's sql change type and Flyway's Java migrations can all do anything. Treat them as code that needs the closest look.
The six checks
This is the core of the review. It works with a pen and the change script. Each check has what to look for, an illustrative example, and what a good outcome looks like. The SQL below is illustrative and was written for this post; it is not output from any tool.
1. Sensitive columns without encryption
Look for: new columns, or renamed or retyped columns, whose names or comments suggest regulated or personal data: tax and national identifiers, dates of birth, account and card numbers, health data, addresses, phone numbers, free-text notes that will hold any of these.
-- Illustrative: a new sensitive column stored as plain text
ALTER TABLE customers ADD COLUMN tax_id VARCHAR(20);
ALTER TABLE customers ADD COLUMN date_of_birth DATE;
Questions to ask: Is this data needed at all, or would a token or a truncated value do? If it is needed, where is it encrypted: column-level encryption in the database, encryption in the application before insert, or only disk or volume encryption? Who can decrypt it? Does the column flow into replicas, exports or analytics tables that this migration does not show?
Good outcome: the reviewer records which protection applies and why it is enough, or the migration changes to store an encrypted or tokenised value. Disk encryption alone protects against a stolen disk, not against a reporting role with SELECT.
2. Credential columns without hashing
Look for: columns named for passwords, PINs, API keys, recovery codes or tokens; types that suggest a plain string of modest length; defaults or backfills that copy values from another column.
-- Illustrative: a credential column with no sign of hashing
ALTER TABLE operators ADD COLUMN pin VARCHAR(8) NOT NULL DEFAULT '0000';
Questions to ask: Will the application store a slow, salted hash here, or the raw value? Is the column length consistent with the hash format the application uses? Is there a default value that every row now shares?
Good outcome: the migration names the column for what it holds (for example pin_hash), sizes it for the hash, has no shared default, and the reviewer has checked the application code that writes it.
3. Over-broad grants
Look for: GRANT ALL, grants to PUBLIC, grants on whole schemas when one table is needed, write permissions to roles whose job is reading, and WITH GRANT OPTION.
-- Illustrative: a reporting role that can now change and delete data
GRANT ALL PRIVILEGES ON TABLE customers TO reporting;
Questions to ask: What does this role's job need? If it reads, why can it write? If one table, why the schema? Who will own removing this grant if it was a quick fix?
Good outcome: the grant names the specific privileges and objects (SELECT on named columns or a view), and any exception is approved and recorded with an expiry.
4. New privileged accounts
Look for: CREATE ROLE or CREATE USER with superuser, create role, create database or replication attributes; new roles granted membership in an admin role; passwords set inline in the migration.
-- Illustrative: a new account with far more power than it needs
CREATE ROLE etl_loader WITH LOGIN SUPERUSER;
Questions to ask: Why does this account exist, and who will use it? Why does it need these attributes? Where will its credential live, and does setting it here put it into version control?
Good outcome: account creation happens through your identity and secrets process, not in a migration, or the migration creates a role with the narrowest attributes and the credential comes from outside the repository.
5. Removed audit triggers or logging
Look for: DROP TRIGGER, ALTER TABLE ... DISABLE TRIGGER, dropped audit tables, changed audit functions, and changes to database logging settings made through a migration.
-- Illustrative: the change history for customers stops here
DROP TRIGGER customers_audit ON customers;
Questions to ask: What did this trigger record, and for which data? What replaces it? If this is a temporary disable for a bulk load, is the re-enable in the same migration, and what happens if the migration fails halfway?
Good outcome: audit coverage for sensitive tables is unchanged or replaced by something equal, and any gap is a recorded, time-limited exception.
6. Destructive or unbounded statements
Look for: DROP TABLE, DROP COLUMN, TRUNCATE, and UPDATE or DELETE statements with no WHERE clause or a WHERE that matches far more than intended.
-- Illustrative: an unbounded change to every row
UPDATE accounts SET status = 'inactive';
Questions to ask: Is the data being removed backed up and recoverable? Is the statement meant to touch every row? Does the downgrade or rollback actually restore anything, or does it recreate an empty column?
Good outcome: destructive changes follow an expand and contract sequence, with the data copied or archived first and the drop in a later release, and unbounded statements carry an explicit, reviewed predicate.
Step by step: a migration review you can run this week
You can do all of this without buying anything. It assumes migrations live in the same repository as the application, which is the common case.
- Mark the migration paths as owned. Add the migration directories (for example
db/migration,migrations/versionsor each Django app'smigrationsfolder) to your code owners file, with a reviewer group that includes someone who knows the data classification. A pull request that touches those paths then needs that review. - Show the SQL. For ORM frameworks, add a CI step that prints the SQL for new migrations (Django's
sqlmigrate, Alembic's offline mode) and attaches it to the pull request as an artifact or comment. For SQL-based tools, the file is enough. - Keep a data classification list next to the code. A short file listing which tables and columns hold regulated or personal data. Reviewers check new columns against it, and new sensitive columns get added to it in the same pull request.
- Use the six checks as a pull request template. Add a migration section to the pull request template with one line per check and a place to write "not applicable" or a short reason.
- Run a cheap pattern check in CI. A few lines of grep over changed migration files for
GRANT ALL,TO PUBLIC,SUPERUSER,DROP TRIGGER,DISABLE TRIGGER,TRUNCATEandDROP TABLEwill catch the obvious cases and force a human to look. It will also miss anything written differently, so treat it as a tripwire, not a review. - Separate the migration role from the application role. The account that applies migrations should not be the account the application runs as. This limits what a bug in application code can do, and it makes grants in migrations stand out.
- Record the decision. The pull request approval is the record. Make sure it names the reviewer, links the template answers, and is kept for as long as your audit period requires. For exceptions, record who approved, why and until when.
- Re-check after deploy. Compare the live schema and grants with what you expected. Drift happens when someone runs a hotfix by hand.
Template: migration security review
Copy this into your pull request template under a "Database changes" heading.
| Check | Answer | Notes |
|---|---|---|
| New or changed columns holding sensitive data? | Yes / No | Which columns, and how they are protected at rest |
| Credential columns? | Yes / No | Hash algorithm used by the writing code |
| Grants changed? | Yes / No | Role, objects, privileges, and why each is needed |
| Accounts or roles created or altered? | Yes / No | Attributes, owner, where the credential lives |
| Audit triggers, audit tables or logging changed? | Yes / No | What replaces them, and for how long any gap lasts |
| Drops, truncates, or updates or deletes without a narrow WHERE? | Yes / No | Backup, expand and contract plan, expected row count |
| SQL reviewed (for ORM migrations)? | Yes / No | Link to generated SQL |
| Exceptions | None / Listed | Approver and expiry for each |
Decision table: block, warn or let it through
| Finding | Default action | When to accept with an exception |
|---|---|---|
| New sensitive column, no encryption or tokenisation | Block | Data is test or synthetic only, and the table never reaches production |
| Credential column without hashing | Block | Rarely. Prefer changing the migration |
GRANT ALL or grant to PUBLIC | Block | Short-lived, with an expiry and a ticket to narrow it |
| New role with superuser or equivalent | Block | Break-glass role managed outside the app, approved by the database owner |
| Audit trigger removed or disabled | Warn, then require an exception | Replaced by an equal mechanism in the same release |
| Destructive statement | Warn | Data archived first, in an expand and contract sequence |
| Unbounded update or delete | Warn | Intended to touch every row, with a reviewed row count |
These defaults are a starting point. A team with mature database ownership might warn on more; a team with regulated data in many tables might block on more.
Who should review a migration
The best reviewer knows both the data and the database. In practice that is rarely one person, so split the job.
- The author fills in the template and generates the SQL.
- A peer engineer reviews correctness and performance: locks, index builds, backfill time.
- A data or database owner reviews grants, roles and anything touching sensitive tables.
- Security reviews when the template flags a sensitive column, a credential column, a grant, a role or an audit change, rather than every migration.
This keeps security review focused on the changes that carry security impact, which is what CM-4 asks for, without making every column rename wait for the security team.
How to handle generated migrations
Generated migrations deserve their own rule, because they are where security impact hides in plain sight.
- Read the generated operations. Django and Alembic both produce readable files. Autogenerate is a draft. Alembic's documentation is explicit that autogenerate output should be reviewed and corrected, because it cannot detect every change.
- Watch for type changes. A model field that changes from an encrypted type to a plain string will generate a migration that looks harmless.
- Watch for data migrations.
RunPythonand similar operations copy and transform data. A data migration that copies a protected column into an unprotected one is a common way sensitive data leaks across a schema. - Squashed migrations hide history. When a team squashes migrations, the security-relevant operations from many files end up in one. Review the squashed file as if it were new.
How to review a Liquibase changelog or Flyway folder you inherited
Sometimes the question is not "is this change safe" but "what has this schema been doing for five years". A one-off review of the whole folder is worth doing when you take over a service or prepare for a first audit.
- List every migration in order, using the tool's own history table as the source of what actually ran.
- Search the whole set for the six patterns, not just the latest files. Grants and roles created years ago are still live unless a later migration removed them.
- Build the current picture: which roles exist, what they can do, which sensitive columns exist and how they are protected, which audit triggers exist.
- Compare that picture with the live database. Anything in the database that no migration created was done by hand and needs an owner.
- Write the findings down with CWE identifiers so they can be tracked like any other vulnerability.
How Safeguard does this
Safeguard handles migration review through its DB Change-logs integration (GA), which sits on the Integrations page with the other targets and connected inputs. The integration card describes its purpose as reviewing database migrations and change logs for security impact. What follows is the flow as the Integrations page and Safeguard's docs describe it. Where the docs do not show a screen or command, this post describes the outcome rather than inventing one.
- Open Integrations. Safeguard's integrations docs describe adding sources from the Integrations page in the console. The DB Change-logs card is listed there alongside URL, API, mobile, database and cloud-account inputs.
- Upload your change scripts. The card takes uploaded change scripts. A .zip of your migrations directory works too, so you can upload a whole Flyway folder, a Liquibase changelog set or an Alembic versions directory at once.
- Mixed formats are fine. The card lists SQL, Liquibase, Flyway, Alembic and ORM migrations. The migration framework is detected from each file, so a set that mixes, for example, plain SQL hotfix scripts with framework migrations does not need to be split first.
- Read the findings. The change scripts are reviewed for sensitive columns without encryption, credentials with no hashing, over-broad grants, new privileged accounts, removed audit triggers, and destructive or unbounded statements. Those are the same categories as the six checks above.
- Use the CWE and control mapping. Each finding is mapped to a CWE and to the compliance controls it bears on. That gives the reviewer a stable weakness name and gives the compliance lead a direct link from a schema finding to the control it affects, which is the connection CM-4 evidence needs.
- Set policy. Safeguard's policies and gates (GA) decide what happens when a finding meets a rule. The docs describe three actions for a failed policy: Block fails the gate, Warn lets the gate pass with a warning that names the policy, and Notify lets it pass and sends a notification to the workspace's configured channels. Approvals run through exceptions: a developer requests one against a policy and finding, gives a business justification and an expiration date, and an approver grants or denies it. A team can, for example, block on a new privileged account and warn on a destructive statement, mirroring the decision table above, and route accepted risks through a recorded exception rather than an informal thumbs up.
- Fix through your normal process. Findings are tracked like other findings in Safeguard, so the fix is a change to the migration (or a follow-up migration) that goes back through review. Autonomous remediation (GA) can open fix pull requests for supported finding types, and it ships switched off with every category set to Manual; a team decides whether and where to turn it on.
Where the CLI and CI fit
The Safeguard CLI (GA) and its CI scan cover the repository side of the same pull request. The CI docs show the scan command and its gate flags:
curl -fsSL https://cli.safeguard.sh/install | bash
SAFEGUARD_API_KEY=... safeguard ci scan --wait --fail-on critical \
--repository-url "$REPO_URL" --branch "$BRANCH" --commit "$COMMIT"
That snippet is copied from the docs and was not run for this post. The docs list --fail-on-gate with --wait to fail the step when a named policy gate, its id, or all is not Passed or Warning, and --local to scan a checkout on the runner so only results leave it. The same docs say the CI scan covers SAST, SBOM and dependency vulnerabilities. They do not list the DB Change-logs review as part of ci scan, so treat the migration review as the upload flow above and the CI scan as the dependency and code side of the same change. If you need both on every pull request, ask Safeguard support how to wire the upload into your pipeline rather than assuming the CI scan already does it.
A worked example
This example is illustrative, with invented names, and does not describe any customer.
A team uploads a .zip of its migrations/versions folder from an Alembic project plus a handful of hand-written SQL hotfix scripts. The review returns findings for a revision that adds a tax_id column as plain text, a hotfix that grants all privileges on customers to a reporting role, and an older revision that dropped an audit trigger. Each finding carries a CWE and the controls it bears on. The team's policy blocks the plain text column and the grant, and warns on the trigger. The security lead approves a time-limited exception for the trigger while a replacement audit mechanism ships, and the record of that exception, with its approver and expiry, is what goes into the audit evidence folder.
Requirement, what Safeguard does, what you still own
| Requirement | What Safeguard does (GA) | What you still own |
|---|---|---|
| Analyse security impact before a change (CM-4) | Reviews uploaded change scripts for the six categories and maps findings to CWE and controls | Deciding whether the impact is acceptable, and doing the analysis for anything outside those categories |
| Approve or disapprove changes and record the decision (CM-3) | Policies and gates with Block, Warn and Notify; approvals through exceptions | Your change approval process and its retention |
| Least privilege (AC-6) | Flags over-broad grants and new privileged accounts in change scripts | Designing roles, and checking the live database for grants made outside migrations |
| Audit logging (AU-2, AU-12; HIPAA 164.312(b)) | Flags removed audit triggers in change scripts | Choosing and running your audit mechanism, and log retention |
| Protect data at rest (SC-28; HIPAA 164.312(a)(2)(iv)) | Flags sensitive columns without encryption and credentials without hashing | Key management, encryption design and the application code that writes the data |
| Review code that changes production (SSDF PW) | Adds the migration review to the same platform as code and dependency review | Human review of logic, performance and data correctness |
What Safeguard does not do
- It does not run, apply or roll back your migrations. The DB Change-logs integration reviews the scripts you upload. Your migration tool and your deployment process still apply them, and your team owns the result.
- It does not judge performance or locking. Whether an index build will block writes for an hour, or a backfill will fit your maintenance window, is outside what the card describes. Test that against realistic data volumes.
- It reviews the scripts, not the live database. A grant made by hand in a console, or a trigger dropped outside any migration, will not appear in a change-log upload. Safeguard lists a separate live database posture input for TLS, authentication rules, grants, audit logging and encryption at rest; this post does not cover it.
- It does not decide for you. A finding is a reason to look, not a verdict. Approving an exception, accepting a risk, and signing off a change stay with your reviewers and data owners.
- The CI scan does not include the change-log review per the docs. The CI docs list SAST, SBOM and dependency vulnerabilities for
ci scan. Plan the migration review as its own step. - It does not know your data classification. A column named
notesthat holds health information looks harmless by name. Keep your own classification list and review against it. - It does not replace legal or audit judgement. The control mapping shows which controls a finding bears on. Whether your evidence satisfies an auditor or examiner is their call and your compliance lead's.
Common mistakes
- Reviewing the model, not the migration. The model diff says a field was added. Only the migration and its SQL say what grants, defaults and triggers came with it.
- Trusting rollback. A
downgrade()that recreates a dropped column does not bring its data back. - Fixing a failing job with a broad grant. It works today and stays for years. Grant the specific privilege, or record an exception with an expiry.
- Creating accounts in migrations with inline passwords. The credential ends up in version control and in every developer's checkout.
- Disabling triggers for a bulk load and forgetting to re-enable them. If the migration fails after the disable, the re-enable never runs.
- Reviewing only new migrations after taking over a service. Grants and roles from old migrations are still live.
- Treating generated migrations as safe. Autogenerate drafts are drafts.
- Keeping the review in chat. A thumbs up in a channel is not a change record an auditor can rely on.
- Forgetting where the data flows. A protected column copied into an analytics table by a data migration is no longer protected.
FAQ
What should I check in a database migration before merging?
Check six things: new sensitive columns and how they are protected at rest, credential columns and whether they hold hashes, grants and whether they are wider than needed, new roles and their attributes, audit triggers or logging being removed, and destructive or unbounded statements. Write the answers in the pull request so there is a record.
Do ORM migrations need security review if the ORM generates them?
Yes. The generator writes what the model says, and the model knows nothing about data classification or least privilege. Print the SQL with your framework's tools, such as Django's sqlmigrate or Alembic's offline mode, and review that.
How do I review a Liquibase changelog for security?
Review each changeset, because that is the unit Liquibase tracks. For SQL changelogs, read the SQL. For XML, YAML or JSON changelogs, run Liquibase's update-sql command to inspect the SQL it will run, then apply the six checks. Pay extra attention to raw sql change types.
How do I review Flyway migrations for security?
Read each versioned and repeatable migration in order, and give Java migrations the same review as application code. Use the schema history table to confirm what actually ran in each environment.
Is dropping an audit trigger a compliance problem?
It can be. Audit logging controls such as NIST SP 800-53 AU-2 and AU-12, and HIPAA's audit controls at 45 CFR 164.312(b), expect you to record activity on sensitive systems. Removing a trigger without an equal replacement creates a gap you cannot fill later. Check with your compliance lead for your own obligations.
Which CWE applies to an unencrypted sensitive column?
CWE-311, Missing Encryption of Sensitive Data, is the usual fit. For passwords stored in plain text, CWE-256 applies, and CWE-916 covers password hashes that use too little computational effort.
Can Safeguard review a whole migrations folder at once?
Yes. The DB Change-logs card accepts uploaded change scripts, and a .zip of your migrations directory works too. The framework is detected from each file, so a mixed set is fine.
Does Safeguard block a pull request because of a risky migration?
Safeguard's policies and gates support Block, Warn and Notify actions, with approvals through exceptions. The CI docs show --fail-on-gate for failing a pipeline step on a gate, and list SAST, SBOM and dependency vulnerabilities as what ci scan covers. Check the current docs for how the change-log review connects to your pipeline before relying on a gate there.
Does Safeguard check my live database too?
The DB Change-logs integration reviews scripts, not the running database. Safeguard lists a separate live database posture input for TLS, authentication rules, grants, audit logging and encryption at rest.
How do I show an auditor that migrations were reviewed?
Keep the pull request approval with the completed review template, any exception with its approver and expiry, and any tool findings with their CWE and control mapping. Together they show that the change was analysed for security impact and that a named person decided.
Related reading
- The migration is the riskiest part of the pull request
- Expand and contract database migrations and rollback
- Separate the database migration role from the app role
- Production admin tasks, migrations and consoles
A schema change is one kind of fix; the dependency under the code that runs it is another. When the weakness is not in your migration but in a library your service or your migration tooling depends on, the same review discipline applies to getting the fix into production quickly.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.
3. Gold Registry: start from components that are already fixed
Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
The demos below show Safeguard's general flows. None of them is a recording of the DB Change-logs upload, so read them as the surrounding workflow: scanning, reviewing findings and acting on them.
The first demo is a real Safeguard CLI session. It installs the CLI, opens the interactive terminal with safeguard, and asks it to fix a lodash CVE in a repository; the CLI reads the finding and advisory, asks before any command that changes something, edits package.json, runs the install and tests with approval, prints a summary and pushes nothing. It is a dependency fix, not a migration review, but it shows the same habit this post argues for: a change is proposed, a person approves it, and nothing ships without review.
Click through it at your own pace, or open it full screen.
The second demo sets up the local runner (GA) on your own machine, approves it with its device code, and scans from the terminal. Teams whose repositories Safeguard cannot reach from the cloud can use the runner for the code and dependency side of the same pull request, with results uploaded to the console.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to the Safeguard MCP server (GA) with OAuth and chooses which tools Claude can use. A reviewer who already works in Claude can then ask about findings in the workspace without switching screens, limited to the tools that were allowed.
Click through it at your own pace, or open it full screen.
Quick check
A migration drops an audit trigger on a table holding customer records so a bulk load runs faster. What should the review record?
What the trigger recorded, what replaces it and for how long any gap lasts, and an approved, time-limited exception if a gap is accepted. Audit controls such as NIST SP 800-53 AU-2 and AU-12 expect that activity on sensitive data is recorded, and a gap cannot be filled after the fact.
Why is reviewing the Django model diff not enough?
The model diff does not show the SQL that will run, or any grants, defaults, triggers or data migrations that come with it. Print the SQL with sqlmigrate and review the migration itself.
Sources
- Safeguard docs: Integrations
- Safeguard docs: ESSCM integrations
- Safeguard docs: File uploads
- Safeguard docs: Policies and gates
- Safeguard docs: Policy outcomes and actions
- Safeguard docs: CI/CD integration
- Safeguard docs: GitHub Actions
- Safeguard docs: Security posture
- NIST SP 800-53 Rev. 5 control catalog (OSCAL)
- NIST SP 800-218, Secure Software Development Framework
- NIST SP 800-218 (PDF)
- 45 CFR 164.312, Technical safeguards (eCFR)
- CWE-311: Missing Encryption of Sensitive Data
- CWE-256: Plaintext Storage of a Password
- CWE-916: Use of Password Hash With Insufficient Computational Effort
- CWE-269: Improper Privilege Management
- CWE-778: Insufficient Logging
- OWASP Database Security Cheat Sheet
- OWASP Password Storage Cheat Sheet
- Flyway documentation: Migrations
- Liquibase documentation: Changelogs
- Liquibase documentation: DATABASECHANGELOG table
- Liquibase documentation: update-sql
- Alembic tutorial
- Alembic: Auto generating migrations
- Django documentation: Migrations
To try it, download Safeguard and read the docs.
Last reviewed: 25 October 2026. Product capabilities change; check docs.safeguard.sh for the current state.