Back to Blog
ProductGolangGo SecurityTaint AnalysisOpen Source SAST

Code Pathfinder now speaks Go

Shivasurya
Shivasurya
Author and Maintainer

Code Pathfinder now speaks Go — type-aware Go security analysis

Two weeks ago I wrote up CVE-2026-33186, an authorization bypass in grpc-go where a single missing slash in the HTTP/2 :path pseudo-header skipped interceptor checks. The fix in upstream was two lines. What took me longer was writing the detection rule for it.

The vulnerable pattern is specific: an interceptor that calls ServerTransportStream.Method() and string-compares the result without normalizing the leading slash. The rule needed to flag only that call site, only on a receiver of type google.golang.org/grpc.ServerTransportStream, and nothing else. Pattern matching can't do that. Method() is defined on half the interfaces in the Go standard library, so a grep-based scanner buries the real finding under hundreds of unrelated hits.

That gap is what pushed me to finish Go support in Code Pathfinder. It ships today in v2.1.0: 21 security rules, 204 Go classes documented in the SDK reference (66 with verified source/sink annotations, covering Gin, Echo, Fiber, GORM, sqlx, gRPC, and the Go standard library). Warm scans are 6.6x faster than cold.

How Go analysis is different from Python

Go has a method-naming problem. Raw() means one thing on a gorm.DB and a completely different thing on an html/template. Method() is defined on half the interfaces in the standard library. Execute() shows up wherever something can be run. A SAST tool that doesn't read types has to choose between flagging every hit as a finding (noise) or ignoring the whole surface (false negatives).

The compiler already knows the concrete type behind every value. Go is static and interface-driven: any struct with the right method set satisfies an interface with no declaration needed. The type information is sitting in the AST the whole time. Most Go SAST tools just don't read it. They grep for Raw() and emit a finding per hit, and the noise pile keeps growing. Security teams tune out by day two.

Python had the opposite problem. Everything is dynamic, so name matching is often all you have. Go gives you the compiler for free. You just have to use it.

QueryType uses it. Every source and sink in a Go rule is bound to a fully-qualified Go module path. GoGormDB is bound to gorm.io/gorm. GoGRPCServerTransportStream is bound to google.golang.org/grpc. When the engine walks the call graph, it resolves the concrete receiver type on each method call and only matches when that type matches an entry in the FQN list. A Method() call on anything that isn't a grpc server transport stream gets dropped silently.

You can audit whether a rule is actually using type inference. Change fqns to a garbage string and re-run the rule. If findings drop to zero, type inference is load-bearing. If findings survive, the rule fell back to pattern matching and needs tightening. That's the test I run against every rule we ship.

The incremental cache

Large Go codebases are slow to scan cold. File-local work (extracting statements, building CFGs, inferring types) caches cleanly. Hash the file, store the output in SQLite, skip the file next time if nothing changed.

Cross-file resolution is trickier. A file that hasn't changed still needs re-analysis if one of its callees changed. Each file tracks which external functions it calls, and if any of those changed since the last scan, that file re-runs cross-file resolution. Otherwise it comes from cache. A PR touching 5 files out of 400 runs in roughly the time it takes to process those 5 files plus their direct callers.

The SQLite schema is versioned. When the summary format changes, the schema version bumps and the cache clears itself. No stale reads.

What ships in v2.1.0

21 rules across the categories that show up most in real Go security reviews. Each rule links to its registry page with a live playground.

RuleCategoryDetects
GO-SEC-001SQL injectionUser input into database/sql raw queries
GO-GORM-SQLI-001SQL injectionUser input into GORM Raw() / Exec()
GO-GORM-SQLI-002SQL injectionGORM query builder with string concatenation
GO-SQLI-003SQL injectionUser input into sqlx raw queries
GO-SSRF-001SSRFUser input into go-resty HTTP request methods
GO-SSRF-002SSRFUser input into net/http client calls
GO-SEC-002Command injectionHTTP input into os/exec (Gin, Echo, Fiber, Chi, Mux)
GO-XSS-001XSSUnsafe writes to http.ResponseWriter
GO-XSS-002XSSUnsafe type conversions bypassing html/template
GO-XSS-003XSSUnescaped user input in template rendering
GO-CRYPTO-001Weak cryptoMD5 usage
GO-CRYPTO-002Weak cryptoSHA1 usage
GO-CRYPTO-003Weak cryptoDES usage
GO-CRYPTO-004Weak crypto3DES usage
GO-CRYPTO-005Weak cryptoRC4 usage
GO-JWT-002AuthJWT signature verification skipped (golang-jwt)
GO-NET-001NetworkHTTP server started without TLS
GO-NET-002NetworkgRPC client without transport security
GO-PATH-001Path traversalUser-controlled path into file operations
GO-REDIRECT-001Open redirectUser-controlled URL in redirect calls
GO-SEC-004CredentialsHardcoded credentials in variable assignments

Here's GO-GORM-SQLI-001 running against a vulnerable Gin handler. User input from a query parameter flows directly into db.Raw() without parameterization:

Writing your own Go rules

A Go rule in Code Pathfinder is a Python function decorated with @go_rule. The sources and sinks are QueryType method selectors bound to Go module FQNs.

python
from codepathfinder.go_rule import GoGinContext, GoGormDB, GoStrconv
from codepathfinder import flows
from codepathfinder.presets import PropagationPresets
from codepathfinder.go_decorators import go_rule

@go_rule(
    id="GO-GORM-SQLI-001",
    severity="CRITICAL",
    cwe="CWE-89",
    owasp="A03:2021",
    message="User input flows into GORM Raw()/Exec(). Use db.Raw('SELECT ? ', val) instead.",
)
def detect_gorm_sqli():
    return flows(
        from_sources=[
            GoGinContext.method("Query", "Param", "PostForm", "ShouldBindJSON"),
        ],
        to_sinks=[
            GoGormDB.method("Raw", "Exec"),
        ],
        sanitized_by=[
            GoStrconv.method("Atoi", "ParseInt", "ParseFloat"),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )

GoGinContext resolves to github.com/gin-gonic/gin. GoGormDB resolves to gorm.io/gorm. GoStrconv.method("Atoi") resolves to strconv.Atoi in the standard library. When user input flows from a Gin handler into a GORM Raw call without passing through strconv.Atoi, it reports a finding.

PropagationPresets.standard() enables taint through assignments, function arguments, return values, string concatenation, and format strings. scope="global" extends tracking across file boundaries through the call graph.

66 QueryType classes ship with verified source, sink, and sanitizer annotations. Web frameworks: Gin, Echo, Fiber, gRPC, Gorilla Mux, Chi. Database libraries: database/sql, GORM, sqlx, pgx, mongo-driver, go-redis. HTTP clients: net/http, Resty. Authentication: golang-jwt. Cryptography: md5, sha1, des, rc4, aes, x509. Filesystem: os, os/exec, path/filepath. The rest of the Go SDK reference covers 138 more stdlib and third-party packages as auto-indexed stubs.

SDK documentation and the FQN browser

Writing rules in the dark is painful. You need to know which class wraps which module path, which methods are sources vs. sinks, and whether your receiver type is annotated at all. The SDK documentation is the answer. Every Go class has its own page with the full FQN list, every method with its Go signature, and a verified role (source, sink, sanitizer, or neutral).

For ad-hoc lookups, the FQN browser indexes every class across Go and Python in a single searchable surface. Drop in gorm, gin.Context, or jwt and you land on the class page in one click. Each result shows the method count, security roles, and the canonical import.

Two things that are easy to miss:

  1. Auto-indexed stubs have no security roles yet. If a page shows all methods as neutral with a "method-level security roles have not been annotated" banner, the class came in from the CDN registry and no one has written a taint rule against it. That doesn't mean the class is useless. The FQN and method signatures are still correct, so it's a real target for code-quality rules, architectural linters, library-usage policies, deprecation checks, or any other static analysis you want to run against Go code. Security annotations are just one layer.
  2. The API reference is the lookup table for the primitives themselves (flows, QueryType, .method, .attr, .where, .tracks, PropagationPresets). It's separate from the per-class SDK pages. Use it when you're trying to remember whether .where takes a qualifier or a literal.

If you find a class you need and it's still a stub, annotating it is a small PR: fill in roles on the methods you care about, add an example rule, open a PR. The contributor guide walks through the workflow.

Getting started

Install:

bash
pip install codepathfinder

Scan your Go project:

bash
pathfinder scan --ruleset golang/security --project .

GitHub Actions integration:

yaml
- name: Run Code Pathfinder
  uses: shivasurya/code-pathfinder@v2.1.0
  with:
    ruleset: golang/all

Add it to CI so every pull request gets scanned automatically. Code Pathfinder has integrations for GitHub Actions, GitLab CI, CircleCI, Jenkins, and more.

Browse all 21 Go rules at the Go rules registry. Each rule has a vulnerable/secure example pair and an interactive playground.

The rules and engine are open source. If you're working on Go security and want to contribute rules or framework coverage, that's the place.

Try Code Pathfinder Today

Eliminate false positives and find real security vulnerabilities in your code. Get started in minutes with AI-powered SAST.

Free and open source • Apache-2.0 License

Secure your code with confidence

Eliminate false positives and surface real security issues so developers can focus on building features.

Write to us

Send email

Chat with us

Join discussions

Try it now

Get started