SQL migration tools are some of the most trusted software in your stack. They run at deploy time, with elevated database privileges, against your production schema. And most teams never audit them.
We did.
We pointed Opsera’s security-scan agent at two of the most widely used open-source migration tools, Flyway (Redgate) and golang-migrate, and ran full passes across both repos. Five scanners per repo: gitleaks, grype, semgrep, checkov, hadolint. No cherry-picking. Raw output below.
Why These Two?
Flyway has been around since 2010. Default recommendation in Spring Boot circles, owned by Redgate, running in thousands of enterprise CI pipelines. If you’ve worked in Java and touched a database, you’ve probably used it.
golang-migrate is the standard for Go services. Language-agnostic, supports 20+ databases, shows up in virtually every cloud-native Go stack. The kind of tool that gets added once and quietly runs forever.
Both are well-maintained. Both have active communities. That’s exactly why we scanned them, trusted tools are where assumptions live.
Flyway — Score 87.6/100 (Critical Risk)
1,547 git-tracked files scanned. 270 findings: 1 Critical, 22 High, 243 Medium, 4 Low. The medium count is inflated by 200 plaintext-HTTP documentation links, strip those and real risk drops to High (~70).
The Critical: Groovy 2.4.7 RCE
groovy-all 2.4.7 in flyway-plugins/flyway-gradle-plugin/pom.xml carries GHSA-xphj-m9cc-8fmq. MethodClosure.call() before 2.4.8 allows remote code execution when untrusted input is supplied as a method name. Fix: bump to 2.4.21 (also closes GHSA-rcjj-h6gh-jf3r).
The Highs
Dependency (1): lxml 6.0.4 in flyway-shades/requirements.txt, GHSA-vfmq-68hx-4jfw, XML External Entity. Fix: 6.1.0.
Secrets (5): Gitleaks flagged 5 generic-api-key hits. All are example tokens (abc.1234567890) inside Vault Resolver documentation, not live secrets. Cleanup means either replacing them with <your-vault-token> placeholders or adding a .gitleaks.toml allowlist for documentation/**.
SAST (16, all semgrep ERROR-level):
- 4 ProcessBuilder command injection — ScriptMigrationExecutor.java:174, ExternalProcessRunner.java:66, NativeConnectorsProcessRunner.java:51
- 3 formatted-SQL-string construction — NativeConnectorsSqlite.java:72, NativeConnectorsJdbc.java:238, NativeConnectorsJdbc.java:334
- 5 subprocess(shell=True) in flyway-docker/scripts/ — build_images.py:129, docker_utils.py:141, release.py:79, scan_images.py:47, test_images.py:83
- 4 Docker images running as root — alpine/Dockerfile, base/Dockerfile, aws-secretsmanager-jdbc/Dockerfile
The Mediums that matter
Past the 200 http-link findings, nine real Medium SAST signals: 2 Spring JDBC SQL concat, 2 unsafe reflection with non-constant class names, 1 JDBC SQL concat, 1 ObjectInputStream on untrusted data, 1 urlopen with non-constant URL, 1 docker-compose missing no-new-privileges, 1 docker-compose writable filesystem.
Checkov added 27 IaC findings: 9 Dockerfiles without non-root USER, 9 missing HEALTHCHECK, 6 using latest base tag, 2 GitHub Actions workflows with top-level permissions: write-all (build-pr.yml, build-release.yml), and 1 sudo usage in azure/Dockerfile:15.
Flyway summary
| Severity | Finding | Scanner |
| Critical | groovy-all 2.4.7 RCE (GHSA-xphj-m9cc-8fmq) | Grype |
| High | lxml 6.0.4 XXE (GHSA-vfmq-68hx-4jfw) | Grype |
| High | 5 doc example tokens flagged as secrets | Gitleaks |
| High | 4 ProcessBuilder command-injection sites | Semgrep |
| High | 3 formatted-SQL sites in NativeConnectors* | Semgrep |
| High | 5 subprocess(shell=True) in flyway-docker | Semgrep |
| High | 4 Docker images missing non-root USER | Semgrep |
| Medium | 200 plaintext-HTTP links in documentation | Semgrep |
| Medium | 9 misc SAST (Spring SQLi, reflection, deserialization, compose) | Semgrep |
| Medium | 27 IaC findings (Docker + GitHub Actions) | Checkov |
| Medium | 6 Dockerfile lint warnings | Hadolint |
| Low | 4 Dockerfile informational items | Hadolint |
golang-migrate — Score 72.5/100 (High Risk)
58 findings: 2 Critical, 4 High, 45 Medium, 7 Low.
The Criticals
C1 — gRPC-Go authorization bypass. google.golang.org/grpc v1.74.2 carries GHSA-p77j-4mvh-x3m3. Missing leading slash in the :path pseudo-header lets attackers route requests past auth interceptors that match exact paths. Fix: v1.79.3.
C2 — pgx v5 memory-safety. github.com/jackc/pgx/v5 v5.7.6 carries GHSA-9jj7-4m8r-rfcm. Fixed in 5.9.0; use 5.9.2 to also close the Low-severity placeholder-confusion SQL injection (GHSA-j88v-2chj-qfwx).
The Highs
| CVE | Package | Issue | Fix |
| GHSA-jqcq-xjh3-6g23 | pgproto3/v2 v2.3.3 | DoS in Postgres wire protocol | No fixed version |
| GHSA-78h2-9frx-2jm8 | go-jose/v4 v4.0.5 | JWE decryption panic (DoS) | v4.1.4 |
| GHSA-x744-4wpc-v9h2 | docker/docker v28.3.3+incompatible | Moby AuthZ plugin bypass on oversized bodies | No fixed version; dktesting-only |
| GHSA-hfvc-g4fc-pqhx | otel/sdk v1.40.0 | kenv PATH hijacking on BSD | v1.43.0 |
pgproto3 is the one to watch, marked affected with no fix available. Track upstream or migrate to the pgx/v5 stack.
The SAST pattern: CockroachDB driver SQL formatting
19 occurrences of string-formatted-query in a single file: database/cockroachdb/cockroachdb.go (lines 195, 248, 301, 336, 355, and 14 more). Every one interpolates an identifier name into DDL via fmt.Sprintf.
Important caveat: in a migration tool, DDL identifiers come from the operator’s own config, and Postgres placeholders ($1) can’t be used for identifiers anyway. fmt.Sprintf is structurally necessary here. The fix isn’t parameterization, it’s wrapping identifiers with pq.QuoteIdentifier to neutralize injection via crafted table names.
Other notable Mediums
- Missing TLS MinVersion at database/mysql/mysql.go:189 and source/github_ee/github_ee.go:80. Set tls.Config{MinVersion: tls.VersionTLS12}.
- AWS SDK EventStream decoder DoS (GHSA-xmrv-pmrh-hhx2) in aws-sdk-go-v2/service/s3 v1.27.11 and eventstream v1.4.8.
- Hardcoded Neo4j test credential at database/neo4j/neo4j_test.go:21. Test fixture, not production, but gitleaks still flags it.
- Decompression-bomb risk in go-bindata generated files under source/go_bindata/. Examples/testdata, not runtime.
- IaC: 4 Dockerfiles run as root and miss HEALTHCHECK; Dockerfile.circleci uses latest; .github/workflows/ci.yaml lacks explicit least-privilege permissions.
golang-migrate summary
| Severity | Finding | Scanner |
| Critical | gRPC-Go auth bypass (GHSA-p77j-4mvh-x3m3) | Grype |
| Critical | pgx/v5 memory-safety (GHSA-9jj7-4m8r-rfcm) | Grype |
| High | pgproto3/v2 DoS — no fix available | Grype |
| High | go-jose/v4 JWE panic (GHSA-78h2-9frx-2jm8) | Grype |
| High | Moby AuthZ bypass (GHSA-x744-4wpc-v9h2) | Grype |
| High | OpenTelemetry PATH hijacking (GHSA-hfvc-g4fc-pqhx) | Grype |
| Medium | 19 string-formatted SQL in CockroachDB driver | Semgrep |
| Medium | 2 missing TLS MinVersion (MySQL, GitHub EE) | Semgrep |
| Medium | 2 AWS SDK EventStream DoS | Grype |
| Medium | Moby plugin privilege off-by-one | Grype |
| Medium | 2 decompression-bomb risks in go-bindata | Semgrep |
| Medium | Neo4j test credential | Gitleaks |
| Medium | 9 Dockerfile / 1 GHA IaC findings | Checkov |
| Medium | 7 Dockerfile lint warnings | Hadolint |
| Low | 2 pgx placeholder-confusion SQL injection | Grype |
| Low | 5 Dockerfile informational | Hadolint |
The Pattern Across Both Repos
Neither tool is broken. Both are actively maintained by people who know what they’re doing. But across ten scans, the same themes keep showing up:
Transitive dependency drift. Flyway is carrying Groovy 2.4.7 (2016-era) with an RCE that’s been patched for years. golang-migrate has two Criticals and four Highs in modules that all have fixes available (except pgproto3). These aren’t zero-days. They’re known advisories sitting in pom.xml and go.mod waiting for someone to run go get or bump a version.
String interpolation into SQL, justified or not. Flyway’s NativeConnectorsJdbc.java has the pattern in three places. golang-migrate’s CockroachDB driver has it in nineteen. For migration tools specifically, most of this is structurally necessary, you can’t parameterize DDL identifiers. But “necessary” and “safe” aren’t the same thing. Identifier validation or quoting helpers (pq.QuoteIdentifier) are the difference.
Container hygiene. Both repos ship Dockerfiles running as root. Both miss HEALTHCHECK. Both pull unpinned base images or packages. The mechanism to fix is identical in both codebases and takes less than an hour per repo.
GitHub Actions over-permissioned. Flyway’s build-pr.yml and build-release.yml are permissions: write-all at the top level. golang-migrate’s ci.yaml has no explicit permissions block at all. Both default to way more than they need.
What neither scan found is worth naming: no live production secrets, no PII in DDL, no broken auth in the migration logic itself. The foundational hygiene is solid. The risk lives in the layers around the tool, and those layers are where most of us stop looking.
What This Means If You Use Either Tool
If you use Flyway:
- Bump groovy-all to 2.4.21 in flyway-plugins/flyway-gradle-plugin/pom.xml. Closes the Critical.
- Bump lxml to ≥6.1.0 in flyway-shades/requirements.txt.
- Lock permissions: to contents: read at the top of build-pr.yml and build-release.yml.
- Add a non-root USER + HEALTHCHECK to the Dockerfiles you actually ship.
If you use golang-migrate:
Single go get run clears 2 Critical + 3 High + 2 Medium + 1 Low:
go get \
google.golang.org/[email protected] \
github.com/jackc/pgx/[email protected] \
github.com/go-jose/go-jose/[email protected] \
go.opentelemetry.io/otel/[email protected] \
github.com/aws/aws-sdk-go-v2/service/s3@latest \
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream@latest
go mod tidy
Track pgproto3 upstream, no fix yet. If you use the CockroachDB driver, wrap identifiers with pq.QuoteIdentifier. In regulated environments, add MinVersion: tls.VersionTLS12 to the MySQL and GitHub EE TLS configs.
For both:
The compliance story (SOC2, PCI-DSS, GDPR) doesn’t live in the migration tool’s source. It lives in how you deploy it — who has access, what role runs the migrations, whether changes are reviewed before they hit production. The tool gives you the mechanism. The controls are yours.
How We Did This
Opsera’s security-scan agent, run through Cursor against local clones of both repos. Five scanners per repo: gitleaks (secrets), grype (dependency CVEs), semgrep with p/default + p/golang + p/security-audit (SAST), checkov (IaC), hadolint (Dockerfile lint). Scores computed by a fixed log-weighted formula, not manual selection. The findings above are the raw output.
Run it on your own migration scripts, or the tools your team depends on, at opsera.ai/agents.
One More Thing
Both Flyway and golang-migrate have solid docs on how to run migrations. Neither has much on how to run them securely, least-privilege DB roles, secrets management in CI, TLS configuration, change control on the migrations repo itself. The gap between speed and security isn’t just a bottleneck, it’s where silent risks turn into real-world failures. Don’t let your team operate in the dark.Stop accumulating risk and start scaling safely with Opsera AppSec agents. Try them now opsera.ai/agents.



