[sergo] Sergo Report: yaml-injection-custom-jobs-plus-qmd-runson-audit - 2026-03-27 #23270
Closed
Replies: 1 comment
-
|
This discussion was automatically closed because it expired on 2026-03-28T20:30:05.503Z.
|
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Overview
Today's analysis extended the sustained YAML injection audit series (runs 30–32) into previously unexplored compilation paths: the generic custom-job rendering layer (
jobs.go) and QmdConfig indexing job generation (qmd.go). The cached component deepened the injection surface survey to show that custom jobruns-on,env,outputs, andwithfields are all rendered without YAML-safe escaping. The new component revealed the same unescaped string concatenation pattern in the QMD tooling path. All three findings are actionable injection vectors that a workflow author could trigger with a YAML double-quoted string containing a\nescape sequence.Tools Snapshot
find_symbol,search_for_pattern,grep, direct file reads,bashfor targeted line inspectionStrategy Selection
Cached Reuse Component (50%)
Previous Strategy Adapted:
double-quote-injection-deepdive-plus-yaml-string-value-audit(run 31, score 8) andyaml-injection-skip-roles-plus-unicode-truncation-audit(run 32, score 8)%sor string concatenation without YAML escaping.compiler_yaml.go/compiler_pre_activation_job.gofrontmatter env vars to the job rendering layer — thejobs.go:ToYAML()method and the custom job compilation path incompiler_jobs.go.New Exploration Component (50%)
Novel Approach: QmdConfig injection audit — tracing the
qmd.runs-onfrontmatter field throughtools_parser.go→WorkflowData.QmdConfig→qmd.go:buildQmdIndexingJob→jobs.go:ToYAML.pkg/workflow/qmd.go,pkg/workflow/safe_jobs.go,pkg/workflow/compiler_jobs.go:555-580,pkg/workflow/jobs.go:245-370Codebase Context
pkg/: 622 (excluding test files)pkg/workflow— jobs rendering, qmd, compiler_jobs, safe_jobs, jobsFindings Summary
Detailed Findings
Finding 1 — Custom Job
runs-onYAML Injection (HIGH)Files:
pkg/workflow/compiler_jobs.go:561,pkg/workflow/safe_jobs.go:193Description: When compiling custom jobs (non-engine workflow jobs defined in the frontmatter
jobs:block) and safe-output job configurations, the user-suppliedruns-onstring is concatenated directly into a YAML line with no escaping:This string is later written to the compiled GitHub Actions YAML verbatim via
jobs.go:246:Exploit path: A workflow author writes:
Go's
gopkg.in/yaml.v3parses the double-quoted YAML value into a Go string containing literal newlines. The compiled output becomes:…injecting a
strategy:block at the job level.Impact: Allows injecting arbitrary GitHub Actions YAML keys at the job level. Potential misuse: adding
strategy.matrix,environment:(to bypass environment protection rules), orpermissions:overrides.Note: The array/object form of
runs-onusesyaml.Marshaland is safe; only the string scalar form is affected.Finding 2 — Custom Job
env,outputs, andwithYAML Injection (HIGH)File:
pkg/workflow/jobs.go:299,314,340Description: The
Job.ToYAML()method writes custom jobenv:,outputs:, andwith:(reusable workflow inputs) values using raw%sformat with no YAML escaping:These values are populated directly from user YAML frontmatter with no sanitization:
job.Env: fromcompiler_jobs.go:674–678(env:map under a custom job)job.Outputs: fromcompiler_jobs.go:750–757(outputs:map under a custom job)job.With: fromcompiler_jobs.go:769–773(with:map for reusable workflow calls)Exploit path: A workflow YAML with:
Produces compiled YAML output:
ANOTHER_KEYescapes theenv:indentation and becomes a top-level job key (invalid YAML structure or injects a recognized key liketimeout-minutes:).Note:
job.Secretsvalues (line 363) are protected byvalidateSecretsExpression()which enforces the$\{\{ secrets.NAME }}pattern and prevents newline injection.Contrast: Engine
envvalues useyamlStringValue()(engine_helpers.go:224, also unfixed per run 31) which at least attempts quoting for{/[-prefixed values — the custom-job path has zero quoting logic.Finding 3 — QmdConfig
runs-onYAML Injection (MEDIUM)File:
pkg/workflow/qmd.go:628–629Description: The QMD (Query My Docs) tool integration builds a separate
qmd-indexingjob. The job'sruns-onvalue comes from the user'sqmd.runs-on:frontmatter field:This string is assigned to
job.RunsOnand written verbatim atjobs.go:246. Same injection mechanism as Finding 1.Exploit path: A workflow author writes:
Injects a
permissions: contents: writeblock into the QMD indexing job, overriding its read-only permissions.Lower severity than Finding 1 because
qmd.runs-onis less commonly configured and QMD is an advanced feature, but the structural vulnerability is identical.Improvement Tasks Generated
Task 1: Sanitize Custom Job
runs-onString ConcatenationIssue Type: YAML Injection
Problem:
compiler_jobs.go:561andsafe_jobs.go:193concatenate user-suppliedruns-onstrings directly without YAML-safe escaping.Locations:
pkg/workflow/compiler_jobs.go:561pkg/workflow/safe_jobs.go:193pkg/workflow/qmd.go:629(same pattern, see Task 3)Impact:
Recommendation: Replace string concatenation with YAML marshaling for the string scalar case:
This matches the existing array/object case which already uses
yaml.Marshal.Validation:
runs-onvalue\n-containing value to confirm injection is preventedsafe_jobs.go:193uses same fixEstimated Effort: Small
Task 2: Quote Custom Job
env,outputs, andwithValues injobs.goIssue Type: YAML Injection
Problem:
jobs.go:299,314,340write user-controlled string values with%s(no quoting) into YAMLenv:,outputs:, andwith:blocks. A value containing a literal newline escapes the indented block and injects top-level job YAML keys.Locations:
pkg/workflow/jobs.go:299(env:values)pkg/workflow/jobs.go:314(outputs:values)pkg/workflow/jobs.go:340(with:string values for reusable workflow calls)Impact:
timeout-minutes,environment,permissions, or other keys outside theenv:blockRecommendation: Use
%qfor values that do not start with a GitHub Actions expression ($\{\{), mirroringengine_helpers.go:yamlStringValue()ortemplatables.go:buildTemplatableBoolEnvVar():Apply the same pattern to
outputs:(line 314) andwith:string case (line 340).Validation:
"foo\nbar: injected"produces escaped YAML output$\{\{ github.event.issue.number }}remain unquotedEstimated Effort: Small
Task 3: Sanitize QmdConfig
runs-oninqmd.goIssue Type: YAML Injection
Problem:
qmd.go:629uses the same"runs-on: " + data.QmdConfig.RunsOnpattern. A user-suppliedqmd.runs-on:with an embedded newline injects YAML into the QMD indexing job.Location:
pkg/workflow/qmd.go:628–629Impact:
Recommendation: Replace with YAML-marshaled form to be consistent with Task 1:
Note: This is the same fix as Task 1, applied to a different caller. Consider extracting a shared
marshalRunsOn(value string) stringhelper.Validation:
qmd.runs-onvaluemarshalRunsOnhelper to DRY across Tasks 1, 2, 3Estimated Effort: Small
Success Metrics
This Run
jobs.go,compiler_jobs.go,safe_jobs.go,qmd.go,compiler_yaml_helpers.go,safe_outputs_config.go,templatables.go,secrets_validation.go)Reasoning
Historical Context
Strategy Performance (Recent)
Cumulative Statistics
mcp-scripts-shell-injection-plus-yaml-analysis(score 9)Recommendations
Immediate Actions
runs-onstring concatenation (High) —compiler_jobs.go:561,safe_jobs.go:193— useyaml.Marshalfor string scalar caseenv/outputs/withvalues (High) —jobs.go:299,314,340— apply%qquoting for non-expression valuesruns-onconcatenation (Medium) —qmd.go:629— same marshal approachLong-term Improvements
The sustained YAML injection audit series (runs 30–33) reveals a systemic pattern: the codebase has no central "YAML-safe value serializer" for the job/step compilation layer. The existing
yamlStringValue()inengine_helpers.gois applied inconsistently (only in one place, with incomplete logic per run 31). AsafeYAMLScalar(s string) stringhelper that handles expression pass-through, proper quoting for literals, and newline detection should be introduced and applied uniformly across all job and step rendering paths.Next Run Preview
Suggested Focus Areas
job.Ifcondition rendering (jobs.go:~580) —job.Ifis set from user YAMLif:conditions incompiler_jobs.go:579; check if newline injection is possible here or in reusable workflow callif:fieldsjob.Usesfor reusable workflow calls —jobs.go:321writesjob.Useswith%s; while GitHub action reference strings are constrained, YAML injection via thejobs:block deserves confirmationStrategy Evolution
Consider a "fix validation" component for future runs — re-check whether findings from runs 30–33 have been remediated, since the unfixed list now has 40+ entries and tracking which are addressed is valuable signal.
References:
Beta Was this translation helpful? Give feedback.
All reactions