Skip to content

taint: skip context.Context arguments during taint propagation to fix false positives#1543

Merged
ccojocar merged 3 commits intosecurego:masterfrom
richardrigby:master
Feb 24, 2026
Merged

taint: skip context.Context arguments during taint propagation to fix false positives#1543
ccojocar merged 3 commits intosecurego:masterfrom
richardrigby:master

Conversation

@richardrigby
Copy link
Copy Markdown
Contributor

@richardrigby richardrigby commented Feb 23, 2026

Summary

  • Fix widespread G703/G704/G705/G706 false positives caused by context.Context propagating taint from *http.Request through to unrelated function return values
  • Add isContextType() helper and apply it at all four argument-scanning sites in the taint engine
  • Harden isContextType to unwrap pointer layers (e.g., *context.Context) before the named-type check
  • Add unit tests for isContextType covering positive, pointer, and negative cases
  • Add integration test sample verifying the r.Context() → service call → w.Write pattern produces no false positive

Problem

The taint engine currently propagates taint through context.Context arguments.
In idiomatic Go HTTP handlers, this creates a cascading false positive chain that
renders the taint-based rules (G703–G706) unusable on real-world codebases.

Consider this common pattern:

func (s *server) Handler(w http.ResponseWriter, r *http.Request) {
    resp, err := s.grpcService.DoSomething(
        r.Context(),  // ← tainted: method call on tainted *http.Request
        &pb.Request{UserId: userID},
    )
    if err != nil {
        if st, ok := status.FromError(err); ok {
            w.Write(utils.JSONErrorMessage(st.Message()))  // ← G705 flagged
        }
    }
    w.Write(resp.GetFileData())  // ← G705 flagged
}

The taint chain works as follows:

  1. r *http.Request — tainted (type source)
  2. r.Context() — tainted (method call on tainted receiver returns tainted value)
  3. s.grpcService.DoSomething(r.Context(), ...) — entire return tainted because r.Context() is a tainted argument, and for interface/external calls the engine conservatively assumes any tainted argument taints the return
  4. err from the gRPC call — tainted (extracted from tainted call)
  5. status.FromError(err)st.Message() — tainted via receiver chain
  6. w.Write(...) — sink reached with tainted data → G705 reported

In a real codebase with this pattern, this produces 137 false positives across 14 files — every single gRPC call site in every HTTP handler, with zero true positives. This is consistent with the category of false positives reported in #1500.

Why context.Context should not propagate taint

context.Context is a control-flow mechanism, not a data-carrying type relevant to taint sinks:

  • It carries deadlines, cancellation signals, and request-scoped metadata (trace IDs, auth tokens for internal service-to-service use)
  • It does not carry user-controlled input data that could flow to HTTP response bodies, file paths, SQL queries, or other taint sinks
  • The Go standard library and ecosystem idiom is to pass ctx as the first argument to virtually every function that does I/O — treating it as a taint vector means every function that accepts a context and returns data gets its return value tainted, which defeats the purpose of taint analysis

Even context.WithValue stores metadata (trace IDs, auth info) that is not user-controlled input. A theoretical scenario where user input is stored in a context value and later written unsanitized to a response is exotic enough that it should not penalize the overwhelmingly common case.

Fix

isContextType helper (taint/taint.go)

Add an isContextType(types.Type) bool helper that:

  1. Unwraps any pointer layers (handles *context.Context as well as context.Context)
  2. Checks the resulting named type's package path ("context") and name ("Context")
func isContextType(t types.Type) bool {
    for {
        ptr, ok := t.(*types.Pointer)
        if !ok {
            break
        }
        t = ptr.Elem()
    }
    named, ok := t.(*types.Named)
    if !ok {
        return false
    }
    obj := named.Obj()
    return obj != nil && obj.Pkg() != nil &&
        obj.Pkg().Path() == "context" && obj.Name() == "Context"
}

Applied at all four taint-propagation sites in isTainted / doTaintedArgsFlowToReturn:

  1. Interface method calls (val.Call.IsInvoke()) — e.g., gRPC service interface calls like s.service.Method(ctx, req)
  2. External static method calls (no SSA body, with receiver) — external library methods
  3. External plain function calls (no SSA body, no receiver) — e.g., status.FromError(err) where the function body isn't available
  4. doTaintedArgsFlowToReturn — the interprocedural analysis that checks whether tainted arguments in internal functions flow to return statements

The receiver taint propagation (e.g., req.URL.Query().Get("name")) is not affected — this fix only skips context.Context when scanning non-receiver arguments for taint propagation to return values.

Tests

Unit tests (taint/analyzer_internal_test.go)

Three new tests for isContextType:

  • TestIsContextTypeWithContextContextcontext.Context (named interface) returns true
  • TestIsContextTypeWithPointerToContextContext*context.Context (pointer-wrapped) returns true
  • TestIsContextTypeRejectsNonContextTypes — table-driven: http.Request, string, wrong-package-same-name, right-package-wrong-name, pointer to non-context type, nil all return false

Integration test (testutils/g705_samples.go)

New SampleCodeG705 entry (expected issues: 0) exercises the pattern from this PR:

type service struct{}

func (s *service) GetData(ctx context.Context, id string) ([]byte, error) {
    return []byte("safe data"), nil
}

func handler(w http.ResponseWriter, r *http.Request) {
    svc := &service{}
    data, _ := svc.GetData(r.Context(), "static-id")
    w.Write(data)
}

This is exercised by the existing G705 test in analyzers/analyzers_test.go.

Test plan

  • All 24 gosec packages pass (go test ./...)
  • Taint package: 87 tests pass
  • Analyzers package: 156 Ginkgo specs pass (includes new G705 integration sample)
  • Performance benchmark: 12.6M ns/op vs baseline 33.6M / limit 38.6M — within bounds
  • Tested against a real-world Go HTTP codebase with 32 files / 18,757 lines using gRPC service calls in HTTP handlers: 137 false positives → 0 issues
  • Verified that legitimate XSS detection (e.g., writing r.URL.Query().Get("x") directly to http.ResponseWriter) is not affected — that taint flows through the receiver chain, not through context arguments

Related

ref: #1542

richardrigby and others added 3 commits February 24, 2026 11:24
`context.Context` is a control-flow mechanism (deadlines, cancellation,
request-scoped metadata) that does not carry user-controlled data
relevant to taint sinks. When `request.Context()` is passed to downstream
functions (gRPC clients, DB calls), the taint engine was conservatively
marking all return values as tainted, causing cascading false positives
for G703-G706 in any HTTP handler using the standard Go context pattern.

Add `isContextType()` helper and skip `context.Context-typed` arguments at
all four argument-scanning sites in the taint engine: interface method
calls, external static method calls, external plain function calls, and
`doTaintedArgsFlowToReturn` interprocedural analysis.

Receiver taint propagation (e.g., `req.URL.Query().Get()`) is unaffected.

ref: securego#1542
…ve prevention

Add unit tests for the `isContextType` helper verifying it correctly
identifies context.Context and rejects non-context types (http.Request,
string, wrong package, pointer-wrapped, nil). Add G705 integration test
exercising the HTTP handler + r.Context() pattern that previously caused
false positives.
The isContextType function now properly unwraps pointer layers to
detect context.Context types even when wrapped in pointers
(e.g., *context.Context). This prevents false taint propagation
to function outputs from context arguments. Added comprehensive
test coverage for both direct context.Context and pointer
variants to ensure robust type checking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 24, 2026

Codecov Report

❌ Patch coverage is 78.94737% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.48%. Comparing base (f3e2fac) to head (0926c35).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
taint/taint.go 78.94% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1543   +/-   ##
=======================================
  Coverage   80.48%   80.48%           
=======================================
  Files         104      104           
  Lines        9621     9640   +19     
=======================================
+ Hits         7743     7759   +16     
- Misses       1402     1404    +2     
- Partials      476      477    +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ccojocar ccojocar merged commit eba2d15 into securego:master Feb 24, 2026
8 of 9 checks passed
@samstride
Copy link
Copy Markdown

@ccojocar , any chance we can get a release with this fix?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants