Skip to content

Commit 44cd694

Browse files
pfrederiksenclaude
andcommitted
fix: Use randomized delimiter for GitHub Actions heredoc output
Fix heredoc delimiter injection vulnerability by using a cryptographically random delimiter instead of fixed "EOF" string. Security issue: A config file containing "EOF" on its own line could prematurely terminate the heredoc and potentially inject arbitrary GitHub Actions workflow commands. Changes: - Generate random 16-byte delimiter prefixed with "ghadelimiter_" - Update tests to verify random delimiter format - Add test case specifically for EOF injection attack - Add reference to GitHub Actions multiline string documentation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent e8f3784 commit 44cd694

File tree

2 files changed

+67
-7
lines changed

2 files changed

+67
-7
lines changed

cmd/configdiff/compare.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"crypto/rand"
5+
"encoding/hex"
46
"fmt"
57
"os"
68
"path/filepath"
@@ -279,8 +281,16 @@ func writeGitHubOutputs(outputFile string, hasChanges bool, diffOutput string) e
279281
return err
280282
}
281283

282-
// Write diff-output using heredoc format
283-
if _, err := fmt.Fprintf(f, "diff-output<<EOF\n%s\nEOF\n", diffOutput); err != nil {
284+
// Generate random delimiter to prevent injection attacks
285+
// See: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#multiline-strings
286+
delimiterBytes := make([]byte, 16)
287+
if _, err := rand.Read(delimiterBytes); err != nil {
288+
return fmt.Errorf("failed to generate random delimiter: %w", err)
289+
}
290+
delimiter := "ghadelimiter_" + hex.EncodeToString(delimiterBytes)
291+
292+
// Write diff-output using heredoc format with random delimiter
293+
if _, err := fmt.Fprintf(f, "diff-output<<%s\n%s\n%s\n", delimiter, diffOutput, delimiter); err != nil {
284294
return err
285295
}
286296

cmd/configdiff/main_test.go

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,23 @@ func findSubstring(s, substr string) bool {
308308
return false
309309
}
310310

311+
func splitLines(s string) []string {
312+
var lines []string
313+
var line string
314+
for i := 0; i < len(s); i++ {
315+
if s[i] == '\n' {
316+
lines = append(lines, line)
317+
line = ""
318+
} else {
319+
line += string(s[i])
320+
}
321+
}
322+
if line != "" {
323+
lines = append(lines, line)
324+
}
325+
return lines
326+
}
327+
311328
func TestCompareFilesReturnValue(t *testing.T) {
312329
tmpDir := t.TempDir()
313330

@@ -444,6 +461,12 @@ func TestWriteGitHubOutputs(t *testing.T) {
444461
diffOutput: "Changes:\n Modified: /config/value\n Added: /config/newkey\n Removed: /config/oldkey",
445462
wantErr: false,
446463
},
464+
{
465+
name: "output containing EOF (injection test)",
466+
hasChanges: true,
467+
diffOutput: "Some text\nEOF\ninjected-output=malicious\nMore text",
468+
wantErr: false,
469+
},
447470
}
448471

449472
for _, tt := range tests {
@@ -475,13 +498,40 @@ func TestWriteGitHubOutputs(t *testing.T) {
475498
t.Errorf("Output missing expected has-changes line: %q", expectedHasChanges)
476499
}
477500

478-
// Verify diff-output heredoc format
479-
if !contains(output, "diff-output<<EOF\n") {
480-
t.Error("Output missing diff-output heredoc start")
501+
// Verify diff-output heredoc format with random delimiter
502+
if !contains(output, "diff-output<<ghadelimiter_") {
503+
t.Error("Output missing diff-output heredoc start with random delimiter")
504+
}
505+
506+
// Extract the delimiter and verify it's used correctly
507+
lines := splitLines(output)
508+
var delimiter string
509+
var diffStartIdx int
510+
for i, line := range lines {
511+
if len(line) > len("diff-output<<") && line[:13] == "diff-output<<" {
512+
delimiter = line[13:]
513+
diffStartIdx = i + 1
514+
break
515+
}
481516
}
482-
if !contains(output, "\nEOF\n") {
483-
t.Error("Output missing diff-output heredoc end")
517+
518+
if delimiter == "" {
519+
t.Error("Failed to extract delimiter from output")
520+
} else {
521+
// Verify delimiter ends the heredoc
522+
found := false
523+
for i := diffStartIdx; i < len(lines); i++ {
524+
if lines[i] == delimiter {
525+
found = true
526+
break
527+
}
528+
}
529+
if !found {
530+
t.Errorf("Delimiter %q not found at end of heredoc", delimiter)
531+
}
484532
}
533+
534+
// Verify diff content is present
485535
if tt.diffOutput != "" && !contains(output, tt.diffOutput) {
486536
t.Errorf("Output missing expected diff content: %q", tt.diffOutput)
487537
}

0 commit comments

Comments
 (0)