mirror of
https://gitea.com/gitea/runner.git
synced 2026-08-26 13:57:46 +00:00
fix: mask secrets on every path they leave a job (#1188)
A secret in a matrix value reached the log in the clear:
```yaml
strategy:
matrix:
include: "${{ github.token }}"
```
Chasing that one route is pointless, so this masks every sink a secret leaves a job by: the uploaded log rows and the on-disk `job.log`, both through one choke point in `appendLogRow`; the runner's own log, which is where planning errors like that one land with no job logger in reach; the job logger's stdout under debug logging; job summaries; job outputs; and the job name that becomes a container name.
Values the runner knows but the job never declared, the proxy password and the task token, are hidden the same way. Masks apply longest first, since `strings.Replacer` matches in argument order and one secret prefixing another would otherwise mask the prefix and print the rest.
### What changes for users
An output whose value carries a secret is skipped with a warning instead of sent, matching GitHub. Output that showed a secret now shows `***`. `ACTIONS_STEP_DEBUG` and `ACTIONS_RUNNER_DEBUG` are never masked, also matching GitHub, so an output of `true` still reaches the jobs that need it.
Each fix has a test that fails without it.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1188
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
+51
-8
@@ -122,7 +122,7 @@ func WithJobLogger(ctx context.Context, jobID, jobName string, config *Config, m
|
||||
|
||||
logger.SetFormatter(&maskedFormatter{
|
||||
Formatter: logger.Formatter,
|
||||
masker: valueMasker(config.InsecureSecrets, config.Secrets),
|
||||
masker: valueMasker(config.InsecureSecrets, config.maskers()),
|
||||
})
|
||||
rtn := logger.WithFields(logrus.Fields{
|
||||
"job": jobName,
|
||||
@@ -234,6 +234,17 @@ func jsonStringEscapeNoHTML(v string) string {
|
||||
return string(encoded[1 : len(encoded)-1])
|
||||
}
|
||||
|
||||
// AppendSecretMaskers skips the debug settings, as GitHub does: they arrive as secrets, but
|
||||
// masking "true" would corrupt unrelated log lines and drop job outputs that say it.
|
||||
func AppendSecretMaskers(oldnew []string, secrets map[string]string) []string {
|
||||
for k, v := range secrets {
|
||||
if k != "ACTIONS_STEP_DEBUG" && k != "ACTIONS_RUNNER_DEBUG" {
|
||||
oldnew = AppendSecretMasker(oldnew, v)
|
||||
}
|
||||
}
|
||||
return oldnew
|
||||
}
|
||||
|
||||
func AppendSecretMasker(oldnew []string, v string) []string {
|
||||
ret := oldnew
|
||||
|
||||
@@ -269,13 +280,9 @@ func AppendSecretMasker(oldnew []string, v string) []string {
|
||||
|
||||
// valueMasker applies secrets and ::add-mask:: patterns to every log entry, including
|
||||
// raw_output (command/stream) lines; there is no bypass by field.
|
||||
func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor {
|
||||
var oldnew []string
|
||||
for _, v := range secrets {
|
||||
oldnew = AppendSecretMasker(oldnew, v)
|
||||
}
|
||||
func valueMasker(insecureSecrets bool, oldnew []string) entryProcessor {
|
||||
oldnew = slices.Clip(oldnew)
|
||||
defReplacer := strings.NewReplacer(oldnew...)
|
||||
defReplacer := NewSecretReplacer(oldnew)
|
||||
|
||||
// A ::add-mask:: only ever appends to the job's mask slice, so the replacer built for
|
||||
// it stays valid until the slice grows. Cache it, keyed by the slice itself and its
|
||||
@@ -311,7 +318,7 @@ func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor
|
||||
pairs = AppendSecretMasker(pairs, v)
|
||||
}
|
||||
masked = len(*masks)
|
||||
replacer = strings.NewReplacer(pairs...)
|
||||
replacer = NewSecretReplacer(pairs)
|
||||
}
|
||||
cmasker := replacer
|
||||
mu.Unlock()
|
||||
@@ -419,3 +426,39 @@ func checkIfTerminal(w io.Writer) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// maskSecrets hides this job's secrets in a value that reaches somewhere the log maskers cannot,
|
||||
// such as a container name or a job summary. Masks added at runtime count, so a summary written
|
||||
// after ::add-mask:: is covered too.
|
||||
func (rc *RunContext) maskSecrets(value string) string {
|
||||
oldnew := rc.Config.maskers()
|
||||
for _, mask := range rc.Masks {
|
||||
oldnew = AppendSecretMasker(oldnew, mask)
|
||||
}
|
||||
return NewSecretReplacer(oldnew).Replace(value)
|
||||
}
|
||||
|
||||
// maskers is every value this job's configuration says to hide, whatever the sink.
|
||||
func (c *Config) maskers() []string {
|
||||
oldnew := AppendSecretMaskers(nil, c.Secrets)
|
||||
for _, mask := range c.ExtraMasks {
|
||||
oldnew = AppendSecretMasker(oldnew, mask)
|
||||
}
|
||||
return oldnew
|
||||
}
|
||||
|
||||
// NewSecretReplacer masks the longest secret first. Replacer matches in argument order, so a
|
||||
// secret that prefixes another would otherwise mask only that prefix and print the rest.
|
||||
func NewSecretReplacer(oldnew []string) *strings.Replacer {
|
||||
pairs := make([][2]string, 0, len(oldnew)/2)
|
||||
for i := 0; i+1 < len(oldnew); i += 2 {
|
||||
pairs = append(pairs, [2]string{oldnew[i], oldnew[i+1]})
|
||||
}
|
||||
slices.SortFunc(pairs, func(a, b [2]string) int { return len(b[0]) - len(a[0]) })
|
||||
|
||||
sorted := make([]string, 0, len(pairs)*2)
|
||||
for _, pair := range pairs {
|
||||
sorted = append(sorted, pair[0], pair[1])
|
||||
}
|
||||
return strings.NewReplacer(sorted...)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user