mirror of
https://gitea.com/gitea/runner.git
synced 2026-08-25 21:37:45 +00:00
0712b2a7a1
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>
52 lines
1.6 KiB
Go
52 lines
1.6 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package report
|
|
|
|
import (
|
|
"testing"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGlobalMasks(t *testing.T) {
|
|
formatter := MaskingFormatter(&log.TextFormatter{DisableTimestamp: true})
|
|
format := func(job string) string {
|
|
line, err := formatter.Format(log.WithField("job", job))
|
|
require.NoError(t, err)
|
|
return string(line)
|
|
}
|
|
register := func(oldnew ...string) *Reporter {
|
|
r := &Reporter{oldnew: oldnew}
|
|
registerGlobalMasks(r)
|
|
t.Cleanup(func() { deregisterGlobalMasks(r) })
|
|
return r
|
|
}
|
|
|
|
assert.Contains(t, format("build s3cr3t"), "s3cr3t")
|
|
|
|
first := register("s3cr3t", "***")
|
|
second := register("other", "***", "otherlonger", "***")
|
|
|
|
line := format("build s3cr3t and other")
|
|
assert.NotContains(t, line, "s3cr3t") // masked though it rode a field, not the message
|
|
assert.NotContains(t, line, "other")
|
|
assert.NotContains(t, format("otherlonger"), "longer") // longest first, so not "***longer"
|
|
|
|
deregisterGlobalMasks(first)
|
|
line = format("build s3cr3t and other")
|
|
assert.Contains(t, line, "s3cr3t")
|
|
assert.NotContains(t, line, "other") // the task still running keeps its own
|
|
|
|
deregisterGlobalMasks(second)
|
|
assert.Nil(t, globalReplacer.Load())
|
|
|
|
// A workflow could otherwise mask "error" here and rewrite every other task's log.
|
|
third := register("s3cr3t", "***")
|
|
third.addMask("runtime-secret")
|
|
assert.Contains(t, format("saw runtime-secret"), "runtime-secret")
|
|
assert.NotContains(t, third.mask("saw runtime-secret"), "runtime-secret")
|
|
}
|