Skip to content

Commit e255fc7

Browse files
authored
Add compiler check disallowing secrets expressions in custom steps (#24450)
1 parent 773b89e commit e255fc7

3 files changed

Lines changed: 444 additions & 0 deletions

File tree

pkg/workflow/compiler_orchestrator_engine.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ func (c *Compiler) setupEngineAndImports(result *parser.FrontmatterResult, clean
8686
return nil, err
8787
}
8888

89+
// Validate steps/post-steps secrets regardless of strict mode (error in strict, warning in non-strict)
90+
if err := c.validateStepsSecrets(result.Frontmatter); err != nil {
91+
orchestratorEngineLog.Printf("Steps secrets validation failed: %v", err)
92+
// Restore strict mode before returning error
93+
c.strictMode = initialStrictMode
94+
return nil, err
95+
}
96+
8997
// Validate check-for-updates flag regardless of strict mode (error in strict, warning in non-strict)
9098
if err := c.validateUpdateCheck(result.Frontmatter); err != nil {
9199
orchestratorEngineLog.Printf("Update check validation failed: %v", err)
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// This file contains strict mode validation for secrets in custom steps.
2+
//
3+
// It validates that secrets expressions are not used in custom steps (steps and
4+
// post-steps injected in the agent job). In strict mode this is an error; in
5+
// non-strict mode a warning is emitted instead.
6+
//
7+
// The goal is to minimise the number of secrets present in the agent job: the
8+
// only secrets that should appear there are those required to configure the
9+
// agentic engine itself.
10+
11+
package workflow
12+
13+
import (
14+
"fmt"
15+
"os"
16+
"strings"
17+
18+
"github.com/github/gh-aw/pkg/console"
19+
)
20+
21+
// validateStepsSecrets checks both the "steps" and "post-steps" frontmatter sections
22+
// for secrets expressions (e.g. ${{ secrets.MY_SECRET }}).
23+
//
24+
// In strict mode the presence of any such expression is treated as an error.
25+
// In non-strict mode a warning is emitted instead.
26+
func (c *Compiler) validateStepsSecrets(frontmatter map[string]any) error {
27+
for _, sectionName := range []string{"steps", "post-steps"} {
28+
if err := c.validateStepsSectionSecrets(frontmatter, sectionName); err != nil {
29+
return err
30+
}
31+
}
32+
return nil
33+
}
34+
35+
// validateStepsSectionSecrets inspects a single steps section (named by sectionName)
36+
// inside frontmatter for any secrets.* expressions.
37+
func (c *Compiler) validateStepsSectionSecrets(frontmatter map[string]any, sectionName string) error {
38+
rawValue, exists := frontmatter[sectionName]
39+
if !exists {
40+
strictModeValidationLog.Printf("No %s section found, skipping secrets validation", sectionName)
41+
return nil
42+
}
43+
44+
steps, ok := rawValue.([]any)
45+
if !ok {
46+
strictModeValidationLog.Printf("%s section is not a list, skipping secrets validation", sectionName)
47+
return nil
48+
}
49+
50+
var secretRefs []string
51+
for _, step := range steps {
52+
refs := extractSecretsFromStepValue(step)
53+
secretRefs = append(secretRefs, refs...)
54+
}
55+
56+
// Filter out the built-in GITHUB_TOKEN: it is already present in every runner
57+
// environment and is not a user-defined secret that could be accidentally leaked.
58+
secretRefs = filterBuiltinTokens(secretRefs)
59+
60+
if len(secretRefs) == 0 {
61+
strictModeValidationLog.Printf("No secrets found in %s section", sectionName)
62+
return nil
63+
}
64+
65+
strictModeValidationLog.Printf("Found %d secret expression(s) in %s section: %v", len(secretRefs), sectionName, secretRefs)
66+
67+
// Deduplicate for cleaner messages.
68+
secretRefs = deduplicateStringSlice(secretRefs)
69+
70+
if c.strictMode {
71+
return fmt.Errorf(
72+
"strict mode: secrets expressions detected in '%s' section may be leaked to the agent job. Found: %s. "+
73+
"Operations requiring secrets must be moved to a separate job outside the agent job",
74+
sectionName, strings.Join(secretRefs, ", "),
75+
)
76+
}
77+
78+
// Non-strict mode: emit a warning.
79+
warningMsg := fmt.Sprintf(
80+
"Warning: secrets expressions detected in '%s' section may be leaked to the agent job. Found: %s. "+
81+
"Consider moving operations requiring secrets to a separate job outside the agent job.",
82+
sectionName, strings.Join(secretRefs, ", "),
83+
)
84+
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(warningMsg))
85+
c.IncrementWarningCount()
86+
87+
return nil
88+
}
89+
90+
// extractSecretsFromStepValue recursively walks a step value (which may be a map,
91+
// slice, or primitive) and returns all secrets.* expressions found in string values.
92+
func extractSecretsFromStepValue(value any) []string {
93+
var refs []string
94+
switch v := value.(type) {
95+
case string:
96+
for _, expr := range ExtractSecretsFromValue(v) {
97+
refs = append(refs, expr)
98+
}
99+
case map[string]any:
100+
for _, fieldValue := range v {
101+
refs = append(refs, extractSecretsFromStepValue(fieldValue)...)
102+
}
103+
case []any:
104+
for _, item := range v {
105+
refs = append(refs, extractSecretsFromStepValue(item)...)
106+
}
107+
}
108+
return refs
109+
}
110+
111+
// deduplicateStringSlice returns a slice with duplicate entries removed,
112+
// preserving the order of first occurrence.
113+
func deduplicateStringSlice(in []string) []string {
114+
seen := make(map[string]bool, len(in))
115+
out := make([]string, 0, len(in))
116+
for _, s := range in {
117+
if !seen[s] {
118+
seen[s] = true
119+
out = append(out, s)
120+
}
121+
}
122+
return out
123+
}
124+
125+
// filterBuiltinTokens removes secret expressions that reference GitHub's built-in
126+
// GITHUB_TOKEN from the list. GITHUB_TOKEN is automatically provided by the runner
127+
// environment and is not a user-defined secret; it therefore does not represent an
128+
// accidental leak into the agent job.
129+
func filterBuiltinTokens(refs []string) []string {
130+
out := refs[:0:0]
131+
for _, ref := range refs {
132+
if !strings.Contains(ref, "secrets.GITHUB_TOKEN") {
133+
out = append(out, ref)
134+
}
135+
}
136+
return out
137+
}

0 commit comments

Comments
 (0)