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>
68 lines
1.7 KiB
Go
68 lines
1.7 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package report
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"gitea.com/gitea/runner/act/runner"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// A job's plan runs before its job logger exists and logs through the process-wide one, which
|
|
// several tasks share, so that one holds the union of every live task's starting secrets.
|
|
var (
|
|
globalMu sync.Mutex
|
|
globalMasks = map[*Reporter][]string{}
|
|
globalReplacer atomic.Pointer[strings.Replacer] // nil while nothing is registered
|
|
)
|
|
|
|
func registerGlobalMasks(r *Reporter) {
|
|
globalMu.Lock()
|
|
defer globalMu.Unlock()
|
|
globalMasks[r] = r.oldnew
|
|
rebuildGlobalReplacer()
|
|
}
|
|
|
|
func deregisterGlobalMasks(r *Reporter) {
|
|
globalMu.Lock()
|
|
defer globalMu.Unlock()
|
|
delete(globalMasks, r)
|
|
rebuildGlobalReplacer()
|
|
}
|
|
|
|
func rebuildGlobalReplacer() { // caller holds globalMu
|
|
var oldnew []string
|
|
for _, masks := range globalMasks {
|
|
oldnew = append(oldnew, masks...)
|
|
}
|
|
if len(oldnew) == 0 {
|
|
globalReplacer.Store(nil)
|
|
return
|
|
}
|
|
globalReplacer.Store(runner.NewSecretReplacer(oldnew))
|
|
}
|
|
|
|
// MaskingFormatter wraps f so a registered value cannot reach the process-wide log, fields included.
|
|
func MaskingFormatter(f log.Formatter) log.Formatter {
|
|
return &maskingFormatter{inner: f}
|
|
}
|
|
|
|
type maskingFormatter struct{ inner log.Formatter }
|
|
|
|
func (m *maskingFormatter) Format(entry *log.Entry) ([]byte, error) {
|
|
line, err := m.inner.Format(entry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
replacer := globalReplacer.Load()
|
|
if replacer == nil { // nothing to hide, so an idle daemon pays no copy
|
|
return line, nil
|
|
}
|
|
return []byte(replacer.Replace(string(line))), nil
|
|
}
|