#1740·gosec

# nosec 抑制是非确定性的(相同源,相同二进制文件)

作者: crudo创建于 2026年9月9日更新于 2026年9月9日

Running gosec repeatedly over unchanged source produces different results. A G101 finding that is covered by a #nosec G101 directive is correctly suppressed on most runs, but leaks through as a reported issue on a minority of runs — no source change, no flag change, same gosec binary. This makes gosec unusable as a hard CI gate: a job that runs gosec ./... and fails on any HIGH finding flakes red at random.

Environment

  • gosec: v2.29.0 (also reproduces on @latest built from source)
  • Go: go1.26.8 (also seen on 1.22/1.23)
  • OS: reproduced on macOS (arm64) and Ubuntu (GitHub-hosted ubuntu-latest)

Minimal reproduction

Self-contained package (stdlib only, compiles, go vet-clean):

go
package repro
import (
	"fmt"
	"net/url"
	"os"
)
// #nosec G101 -- example connection strings in help text are not real credentials
var serveCmd = &struct {
	Use, Long string
	RunE      func(args []string) error
}{
	Use: "serve",
	Long: `Config file format:
  databases:
    db1:
      url: "postgres://user:pass@host:5432/db1"
    db2:
      url: "postgres://user:pass@host:5432/db2"
`,
	RunE: func(args []string) error {
		cfgPath := os.Getenv("CFG")
		raw, err := os.ReadFile(cfgPath) // #nosec G304 -- operator-supplied path
		if err != nil {
			return fmt.Errorf("read: %w", err)
		}
		_ = raw
		u, _ := url.Parse(os.Getenv("DB_URL"))
		if u != nil && u.User != nil {
			u.User = url.UserPassword(u.User.Username(), "x")
		}
		return nil
	},
}
func main() { _ = serveCmd }

Run gosec ~50–60 times and count the G101 findings each run:

bash
for i in $(seq 1 60); do
  gosec -fmt json -out out.json ./... >/dev/null 2>&1 || true
  python3 -c "import json;print(len([x for x in json.load(open('out.json'))['Issues'] if x['rule_id']=='G101']),end=' ')"
done; echo

A ready-made script (reproduce.sh) that generates the package, runs gosec N times, and exits non-zero when the outcome varies is attached.

Expected

Every run yields the identical result. The #nosec G101 directive covers the declaration, so the G101 finding should be suppressed on every run (count 0).

Actual

The G101 count varies run to run over identical source, e.g.:

1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0

0 = directive honored (correct); 1 = same directive ignored, finding leaks. Leak rate is roughly 5–15% per run and rises with more concurrent CPU load. On the real-world codebase where we first hit this, the same file flapped between 9 and 10 total findings across 50 CI runs — one suppressed G101 appearing/disappearing — which is what led us here.

What appears to matter (from bisecting)

The bug only surfaces with a …