Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion internal/parser/languages/extractor_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,41 @@ func (e *SubprocessExtractor) Extract(filePath string, src []byte) (*parser.Extr
// raw-string literal; the `"` and `\` are escaped.
const shellUnsafePathChars = " ;|&<>()\"'\\$" + "`="

// secretEnvName reports whether an env var name is secret-shaped and must be
// stripped from the plugin subprocess environment, so an extractor command —
// potentially declared in a repo-local .gortex.yaml — can't read it and
// exfiltrate it. It matches the shapes the LLM/embedding/network backends
// actually use (e.g. OPENAI_API_KEY, GORTEX_LLM_API_KEY, GITHUB_TOKEN,
// AWS_SECRET_ACCESS_KEY) rather than an incomplete hardcoded list, so no
// future key escapes by an unknown name. PATH and other non-secret vars are
// inherited normally.
func secretEnvName(name string) bool {
u := strings.ToUpper(name)
if u == "AUTHORIZATION" {
return true
}
return strings.Contains(u, "AUTHORIZATION") ||
strings.Contains(u, "API_KEY") ||
strings.Contains(u, "APIKEY") ||
strings.Contains(u, "_TOKEN") ||
strings.Contains(u, "_SECRET") ||
strings.Contains(u, "_BEARER")
}

// stripSecretEnv returns env without secret-shaped entries. The result is a
// fresh slice; callers append non-secret extras (e.g. GORTEX_FILE_PATH)
// afterwards.
func stripSecretEnv(env []string) []string {
out := make([]string, 0, len(env))
for _, kv := range env {
key, _, ok := strings.Cut(kv, "=")
if !ok || !secretEnvName(key) {
out = append(out, kv)
}
}
return out
}

// ExtractBounded runs the configured plugin with hard wire and result limits.
// It is an all-or-nothing capability: caller cancellation, limit violations,
// command failures, timeouts and malformed output return no partial result.
Expand Down Expand Up @@ -486,7 +521,12 @@ func (e *SubprocessExtractor) ExtractBounded(ctx context.Context, filePath strin
cmd.Stdin = bytes.NewReader(src)
cmd.Stdout = stdout
cmd.Stderr = stderr
cmd.Env = append(os.Environ(), "GORTEX_FILE_PATH="+filePath)
// The plugin runs a command declared in the operator's / repo's
// .gortex.yaml and inherits this environment. Start from os.Environ()
// with the secret-shaped entries removed, then add GORTEX_FILE_PATH:
// otherwise an extractor command could read them (e.g. os.Environ())
// and exfiltrate them. PATH is inherited so the command resolves.
cmd.Env = append(stripSecretEnv(os.Environ()), "GORTEX_FILE_PATH="+filePath)

runErr := cmd.Run()
if err := ctx.Err(); err != nil {
Expand Down
46 changes: 46 additions & 0 deletions internal/parser/languages/extractor_plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,3 +483,49 @@ func TestRegisterExtractorPlugins_SkipsInvalidAndCollisions(t *testing.T) {
require.True(t, ok)
assert.Equal(t, "go", e.Language())
}

// TestStripSecretEnv verifies the plugin subprocess environment drops
// secret-shaped entries (provider API keys, tokens, AWS secrets, bearer
// vars) while keeping PATH and other non-secret vars — so an extractor
// command can't read and exfiltrate them via os.Environ().
func TestStripSecretEnv(t *testing.T) {
in := []string{
"PATH=/usr/local/bin:/usr/bin",
"OPENAI_API_KEY=sk-abc",
"GOOGLE_API_KEY=AIza",
"API_KEY=barekey",
"GOOGLEAPIKEY=concatkey",
"GORTEX_LLM_API_KEY=glp-xyz",
"GORTEX_EMBEDDINGS_API_KEY=e-123",
"GITHUB_TOKEN=ghp_123",
"AUTH_TOKEN=tok",
"AWS_SECRET_ACCESS_KEY=wJalr",
"MY_SECRET=xyz",
"GITHUB_BEARER=b",
"HDR_AUTHORIZATION=Bearer abc",
"APIKEY=plainkey",
"MY_SEGMENTER=split", // not secret-shaped
"GORTEX_FILE_PATH=repo/a.go",
"GITHUB_ACTIONS", // no '=', kept
}
got := map[string]bool{}
for _, kv := range stripSecretEnv(in) {
key := kv
if i := strings.IndexByte(kv, '='); i >= 0 {
key = kv[:i]
}
got[key] = true
}
for _, keep := range []string{"PATH", "MY_SEGMENTER", "GORTEX_FILE_PATH", "GITHUB_ACTIONS"} {
assert.True(t, got[keep], "expected %q to be kept", keep)
}
dropped := []string{
"OPENAI_API_KEY", "GOOGLE_API_KEY", "API_KEY", "GOOGLEAPIKEY",
"GORTEX_LLM_API_KEY", "GORTEX_EMBEDDINGS_API_KEY",
"GITHUB_TOKEN", "AUTH_TOKEN", "AWS_SECRET_ACCESS_KEY", "MY_SECRET",
"GITHUB_BEARER", "HDR_AUTHORIZATION", "APIKEY",
}
for _, name := range dropped {
assert.False(t, got[name], "expected %q to be dropped", name)
}
}
Loading