From 4bf12ac9efef74c49ddefe45c3bb9cacd5d068ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 06:41:48 +0000 Subject: [PATCH 1/5] Initial plan From 28f02e2f24cd8c1753bffc0e7e97f8e3a1bbe807 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 06:47:18 +0000 Subject: [PATCH 2/5] Initial analysis of SQL injection vulnerability in update_project_job.go Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- .github/workflows/smoke-codex.lock.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index fc223cbaa5..e6af4d2124 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -296,6 +296,30 @@ jobs: }, "name": "add_labels" }, + { + "description": "Remove labels from an existing GitHub issue or pull request. Silently skips labels that don't exist on the item. Use this to clean up labels or manage label lifecycles (e.g., removing 'needs-review' after review is complete). CONSTRAINTS: Only these labels can be removed: [smoke].", + "inputSchema": { + "additionalProperties": false, + "properties": { + "item_number": { + "description": "Issue or PR number to remove labels from. This is the numeric ID from the GitHub URL (e.g., 456 in github.com/owner/repo/issues/456). If omitted, removes labels from the item that triggered this workflow.", + "type": "number" + }, + "labels": { + "description": "Label names to remove (e.g., ['smoke', 'needs-triage']). Non-existent labels are silently skipped.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "labels" + ], + "type": "object" + }, + "name": "remove_labels" + }, { "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", "inputSchema": { From ecf251f15c5d7420894b5cfb09d0fbe0f1d35432 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 06:53:57 +0000 Subject: [PATCH 3/5] Fix SQL injection vulnerability in update_project_job.go by escaping JSON values Replace unsafe %q format with proper YAML single-quote escaping using escapeSingleQuote() function. This prevents injection when JSON values containing single quotes or backslashes are embedded in YAML environment variables. Add comprehensive tests for edge cases including single quotes, backslashes, and injection attempts. Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- pkg/workflow/update_project_job.go | 9 +- pkg/workflow/update_project_test.go | 161 ++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 5 deletions(-) diff --git a/pkg/workflow/update_project_job.go b/pkg/workflow/update_project_job.go index 03c48ecb00..47b3ef0cab 100644 --- a/pkg/workflow/update_project_job.go +++ b/pkg/workflow/update_project_job.go @@ -44,11 +44,10 @@ func (c *Compiler) buildUpdateProjectJob(data *WorkflowData, mainJobName string) if err != nil { return nil, fmt.Errorf("failed to marshal views configuration: %w", err) } - // lgtm[go/unsafe-quoting] - This generates YAML environment variable declarations, not shell commands. - // The %q format specifier properly escapes the JSON string for YAML syntax. There is no shell injection - // risk because this value is set as an environment variable in the GitHub Actions YAML configuration, - // not executed as shell code. - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PROJECT_VIEWS: %q\n", string(viewsJSON))) + // Escape single quotes and backslashes for safe embedding in YAML single-quoted strings + // This prevents injection when JSON values contain special characters + escapedJSON := escapeSingleQuote(string(viewsJSON)) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PROJECT_VIEWS: '%s'\n", escapedJSON)) } jobCondition := BuildSafeOutputType("update_project") diff --git a/pkg/workflow/update_project_test.go b/pkg/workflow/update_project_test.go index d3d53aceb1..d4850cc8e1 100644 --- a/pkg/workflow/update_project_test.go +++ b/pkg/workflow/update_project_test.go @@ -324,3 +324,164 @@ func TestUpdateProjectJob_Permissions(t *testing.T) { require.NotEmpty(t, job.Permissions, "Job should have permissions") assert.Contains(t, job.Permissions, "contents: read", "Should have contents: read permission") } + +func TestUpdateProjectJob_ViewsEscaping(t *testing.T) { + tests := []struct { + name string + views []ProjectView + shouldEscape bool + checkContent string + }{ + { + name: "simple views without special characters", + views: []ProjectView{ + {Name: "status", Layout: "board", Filter: "status:Todo"}, + {Name: "priority", Layout: "table", Filter: "priority:High"}, + }, + shouldEscape: false, + checkContent: "status", + }, + { + name: "views with single quotes in filter", + views: []ProjectView{ + {Name: "broken", Layout: "board", Filter: "label:\"can't fix\""}, + {Name: "issues", Layout: "table", Description: "It's broken"}, + }, + shouldEscape: true, + checkContent: `\'`, + }, + { + name: "views with backslashes in description", + views: []ProjectView{ + {Name: "regex", Layout: "board", Description: `Pattern: \d+\.\d+`}, + {Name: "path", Layout: "table", Filter: `path:C:\\Windows`}, + }, + shouldEscape: true, + checkContent: `\\`, + }, + { + name: "views with mixed special characters", + views: []ProjectView{ + {Name: "complex", Layout: "board", Description: `It's a "test" with \backslash`}, + }, + shouldEscape: true, + checkContent: `\\`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compiler := NewCompiler(false, "", "test") + + workflowData := &WorkflowData{ + Name: "test-workflow", + SafeOutputs: &SafeOutputsConfig{ + UpdateProjects: &UpdateProjectConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Max: 10, + }, + Views: tt.views, + }, + }, + } + + job, err := compiler.buildUpdateProjectJob(workflowData, "main") + require.NoError(t, err) + require.NotNil(t, job) + + // Find the step containing GH_AW_PROJECT_VIEWS + var viewsEnvVar string + for _, step := range job.Steps { + if strings.Contains(step, "GH_AW_PROJECT_VIEWS:") { + viewsEnvVar = step + break + } + } + + require.NotEmpty(t, viewsEnvVar, "Should contain GH_AW_PROJECT_VIEWS environment variable") + + // Verify that the JSON is properly escaped + if tt.shouldEscape { + // Should use single-quoted YAML string + assert.Contains(t, viewsEnvVar, "GH_AW_PROJECT_VIEWS: '", "Should use single-quoted YAML string") + + // Verify the expected escape sequences are present + assert.Contains(t, viewsEnvVar, tt.checkContent, "Should contain escaped characters") + } + + // Verify the environment variable is properly formatted as YAML + assert.Contains(t, viewsEnvVar, "GH_AW_PROJECT_VIEWS:", "Should contain environment variable key") + }) + } +} + +func TestUpdateProjectJob_ViewsNoInjection(t *testing.T) { + // Test that malicious input cannot break out of the YAML string + compiler := NewCompiler(false, "", "test") + + maliciousViews := []ProjectView{ + {Name: "injection", Layout: "board", Filter: "'; echo 'injected'; echo '"}, + {Name: "path", Layout: "table", Description: `\'; rm -rf /; echo '`}, + } + + workflowData := &WorkflowData{ + Name: "test-workflow", + SafeOutputs: &SafeOutputsConfig{ + UpdateProjects: &UpdateProjectConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Max: 10, + }, + Views: maliciousViews, + }, + }, + } + + job, err := compiler.buildUpdateProjectJob(workflowData, "main") + require.NoError(t, err) + require.NotNil(t, job) + + // Find the step containing GH_AW_PROJECT_VIEWS + var viewsEnvVar string + for _, step := range job.Steps { + if strings.Contains(step, "GH_AW_PROJECT_VIEWS:") { + viewsEnvVar = step + break + } + } + + require.NotEmpty(t, viewsEnvVar, "Should contain GH_AW_PROJECT_VIEWS environment variable") + + // Verify that all single quotes and backslashes are properly escaped + // The environment variable should be wrapped in single quotes + assert.Contains(t, viewsEnvVar, "GH_AW_PROJECT_VIEWS: '", "Should use single-quoted YAML string") + + // Count opening quotes + openQuotes := strings.Count(viewsEnvVar, "GH_AW_PROJECT_VIEWS: '") + assert.Equal(t, 1, openQuotes, "Should have exactly one opening quote for the environment variable") + + // Verify all single quotes in the JSON are escaped + lines := strings.Split(viewsEnvVar, "\n") + for _, line := range lines { + if strings.Contains(line, "GH_AW_PROJECT_VIEWS:") { + // Extract the value part after the colon + parts := strings.SplitN(line, "GH_AW_PROJECT_VIEWS: '", 2) + if len(parts) == 2 { + value := parts[1] + // Remove the trailing quote + value = strings.TrimSuffix(value, "'") + + // Verify backslashes are escaped (doubled) + if strings.Contains(value, `\`) { + // Should contain escaped backslashes + assert.Contains(t, value, `\\`, "Backslashes should be escaped") + } + + // Verify single quotes are escaped + if strings.Contains(value, "'") { + // Should contain escaped quotes + assert.Contains(t, value, `\'`, "Single quotes should be escaped") + } + } + } + } +} From ab20e9da1e370217a6e85bf013f85b5c07ca9d89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 06:57:31 +0000 Subject: [PATCH 4/5] Fix additional SQL injection vulnerabilities in JSON environment variables Found and fixed multiple instances where %q format was used with JSON values in YAML environment variables: - compiler_safe_outputs_config.go: GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG - Additional 10 files with similar patterns to be fixed All need proper escapeSingleQuote() instead of %q to prevent injection. Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- pkg/workflow/compiler_safe_outputs_config.go | 7 ++++--- pkg/workflow/update_project_test.go | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/workflow/compiler_safe_outputs_config.go b/pkg/workflow/compiler_safe_outputs_config.go index e5ecce541c..6ca8718ead 100644 --- a/pkg/workflow/compiler_safe_outputs_config.go +++ b/pkg/workflow/compiler_safe_outputs_config.go @@ -635,9 +635,10 @@ func (c *Compiler) addProjectHandlerManagerConfigEnvVar(steps *[]string, data *W consolidatedSafeOutputsLog.Printf("Failed to marshal project handler config: %v", err) return } - // Escape the JSON for YAML (handle quotes and special chars) - configStr := string(configJSON) - *steps = append(*steps, fmt.Sprintf(" GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG: %q\n", configStr)) + // Escape single quotes and backslashes for safe embedding in YAML single-quoted strings + // This prevents injection when JSON values contain special characters + escapedJSON := escapeSingleQuote(string(configJSON)) + *steps = append(*steps, fmt.Sprintf(" GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG: '%s'\n", escapedJSON)) } } diff --git a/pkg/workflow/update_project_test.go b/pkg/workflow/update_project_test.go index d4850cc8e1..44fa69db32 100644 --- a/pkg/workflow/update_project_test.go +++ b/pkg/workflow/update_project_test.go @@ -404,7 +404,7 @@ func TestUpdateProjectJob_ViewsEscaping(t *testing.T) { if tt.shouldEscape { // Should use single-quoted YAML string assert.Contains(t, viewsEnvVar, "GH_AW_PROJECT_VIEWS: '", "Should use single-quoted YAML string") - + // Verify the expected escape sequences are present assert.Contains(t, viewsEnvVar, tt.checkContent, "Should contain escaped characters") } @@ -454,11 +454,11 @@ func TestUpdateProjectJob_ViewsNoInjection(t *testing.T) { // Verify that all single quotes and backslashes are properly escaped // The environment variable should be wrapped in single quotes assert.Contains(t, viewsEnvVar, "GH_AW_PROJECT_VIEWS: '", "Should use single-quoted YAML string") - + // Count opening quotes openQuotes := strings.Count(viewsEnvVar, "GH_AW_PROJECT_VIEWS: '") assert.Equal(t, 1, openQuotes, "Should have exactly one opening quote for the environment variable") - + // Verify all single quotes in the JSON are escaped lines := strings.Split(viewsEnvVar, "\n") for _, line := range lines { @@ -469,13 +469,13 @@ func TestUpdateProjectJob_ViewsNoInjection(t *testing.T) { value := parts[1] // Remove the trailing quote value = strings.TrimSuffix(value, "'") - + // Verify backslashes are escaped (doubled) if strings.Contains(value, `\`) { // Should contain escaped backslashes assert.Contains(t, value, `\\`, "Backslashes should be escaped") } - + // Verify single quotes are escaped if strings.Contains(value, "'") { // Should contain escaped quotes From 53e07ac1d7bd03e0d284d16406e12f9cfb305af1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 07:01:16 +0000 Subject: [PATCH 5/5] Fix all remaining SQL injection vulnerabilities in JSON environment variables Completed fixing all instances where %q format was used with JSON values: - add_comment.go: GH_AW_ALLOWED_REASONS - compiler_activation_jobs.go: GH_AW_COMMANDS, GH_AW_SAFE_OUTPUT_MESSAGES - compiler_safe_outputs_job.go: GH_AW_SAFE_OUTPUT_MESSAGES - missing_data.go: GH_AW_MISSING_DATA_LABELS - missing_tool.go: GH_AW_MISSING_TOOL_LABELS - notify_comment.go: GH_AW_MISSING_TOOL_LABELS, GH_AW_SAFE_OUTPUT_MESSAGES (3x), GH_AW_SAFE_OUTPUT_JOBS - safe_outputs_env.go: GH_AW_SAFE_OUTPUT_MESSAGES All now use escapeSingleQuote() with single-quoted YAML strings to prevent injection attacks. Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- pkg/workflow/add_comment.go | 4 +++- pkg/workflow/compiler_activation_jobs.go | 8 ++++++-- pkg/workflow/compiler_safe_outputs_job.go | 4 +++- pkg/workflow/missing_data.go | 4 +++- pkg/workflow/missing_tool.go | 4 +++- pkg/workflow/notify_comment.go | 16 ++++++++++++---- pkg/workflow/safe_outputs_env.go | 4 +++- 7 files changed, 33 insertions(+), 11 deletions(-) diff --git a/pkg/workflow/add_comment.go b/pkg/workflow/add_comment.go index 86e0418cc9..1103318e08 100644 --- a/pkg/workflow/add_comment.go +++ b/pkg/workflow/add_comment.go @@ -61,7 +61,9 @@ func (c *Compiler) buildCreateOutputAddCommentJob(data *WorkflowData, mainJobNam if len(data.SafeOutputs.AddComments.AllowedReasons) > 0 { reasonsJSON, err := json.Marshal(data.SafeOutputs.AddComments.AllowedReasons) if err == nil { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ALLOWED_REASONS: %q\n", string(reasonsJSON))) + // Escape single quotes and backslashes for safe embedding in YAML single-quoted strings + escapedJSON := escapeSingleQuote(string(reasonsJSON)) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ALLOWED_REASONS: '%s'\n", escapedJSON)) } } // Add environment variables for the URLs from other safe output jobs if they exist diff --git a/pkg/workflow/compiler_activation_jobs.go b/pkg/workflow/compiler_activation_jobs.go index 0a67d87f53..686429ca0f 100644 --- a/pkg/workflow/compiler_activation_jobs.go +++ b/pkg/workflow/compiler_activation_jobs.go @@ -148,7 +148,9 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec steps = append(steps, " env:\n") // Pass commands as JSON array commandsJSON, _ := json.Marshal(data.Command) - steps = append(steps, fmt.Sprintf(" GH_AW_COMMANDS: %q\n", string(commandsJSON))) + // Escape single quotes and backslashes for safe embedding in YAML single-quoted strings + escapedJSON := escapeSingleQuote(string(commandsJSON)) + steps = append(steps, fmt.Sprintf(" GH_AW_COMMANDS: '%s'\n", escapedJSON)) steps = append(steps, " with:\n") steps = append(steps, " script: |\n") steps = append(steps, generateGitHubScriptWithRequire("check_command_position.cjs")) @@ -434,7 +436,9 @@ func (c *Compiler) buildActivationJob(data *WorkflowData, preActivationJobCreate if err != nil { compilerActivationJobsLog.Printf("Warning: failed to serialize messages config for activation job: %v", err) } else if messagesJSON != "" { - steps = append(steps, fmt.Sprintf(" GH_AW_SAFE_OUTPUT_MESSAGES: %q\n", messagesJSON)) + // Escape single quotes and backslashes for safe embedding in YAML single-quoted strings + escapedJSON := escapeSingleQuote(messagesJSON) + steps = append(steps, fmt.Sprintf(" GH_AW_SAFE_OUTPUT_MESSAGES: '%s'\n", escapedJSON)) } } diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index b95cd9c7ec..a510a548c5 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -483,7 +483,9 @@ func (c *Compiler) buildJobLevelSafeOutputEnvVars(data *WorkflowData, workflowID if err != nil { consolidatedSafeOutputsJobLog.Printf("Warning: failed to serialize messages config: %v", err) } else if messagesJSON != "" { - envVars["GH_AW_SAFE_OUTPUT_MESSAGES"] = fmt.Sprintf("%q", messagesJSON) + // Escape single quotes and backslashes for safe embedding in YAML single-quoted strings + escapedJSON := escapeSingleQuote(messagesJSON) + envVars["GH_AW_SAFE_OUTPUT_MESSAGES"] = fmt.Sprintf("'%s'", escapedJSON) } } diff --git a/pkg/workflow/missing_data.go b/pkg/workflow/missing_data.go index 88754ee54b..f2b468241d 100644 --- a/pkg/workflow/missing_data.go +++ b/pkg/workflow/missing_data.go @@ -48,7 +48,9 @@ func (c *Compiler) buildCreateOutputMissingDataJob(data *WorkflowData, mainJobNa if len(data.SafeOutputs.MissingData.Labels) > 0 { labelsJSON, err := json.Marshal(data.SafeOutputs.MissingData.Labels) if err == nil { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_MISSING_DATA_LABELS: %q\n", string(labelsJSON))) + // Escape single quotes and backslashes for safe embedding in YAML single-quoted strings + escapedJSON := escapeSingleQuote(string(labelsJSON)) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_MISSING_DATA_LABELS: '%s'\n", escapedJSON)) missingDataLog.Printf("labels: %v", data.SafeOutputs.MissingData.Labels) } } diff --git a/pkg/workflow/missing_tool.go b/pkg/workflow/missing_tool.go index 49fc033c37..c305f1f875 100644 --- a/pkg/workflow/missing_tool.go +++ b/pkg/workflow/missing_tool.go @@ -48,7 +48,9 @@ func (c *Compiler) buildCreateOutputMissingToolJob(data *WorkflowData, mainJobNa if len(data.SafeOutputs.MissingTool.Labels) > 0 { labelsJSON, err := json.Marshal(data.SafeOutputs.MissingTool.Labels) if err == nil { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_MISSING_TOOL_LABELS: %q\n", string(labelsJSON))) + // Escape single quotes and backslashes for safe embedding in YAML single-quoted strings + escapedJSON := escapeSingleQuote(string(labelsJSON)) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_MISSING_TOOL_LABELS: '%s'\n", escapedJSON)) missingToolLog.Printf("labels: %v", data.SafeOutputs.MissingTool.Labels) } } diff --git a/pkg/workflow/notify_comment.go b/pkg/workflow/notify_comment.go index cb292daf49..39f69e432e 100644 --- a/pkg/workflow/notify_comment.go +++ b/pkg/workflow/notify_comment.go @@ -123,7 +123,9 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa if len(data.SafeOutputs.MissingTool.Labels) > 0 { labelsJSON, err := json.Marshal(data.SafeOutputs.MissingTool.Labels) if err == nil { - missingToolEnvVars = append(missingToolEnvVars, fmt.Sprintf(" GH_AW_MISSING_TOOL_LABELS: %q\n", string(labelsJSON))) + // Escape single quotes and backslashes for safe embedding in YAML single-quoted strings + escapedJSON := escapeSingleQuote(string(labelsJSON)) + missingToolEnvVars = append(missingToolEnvVars, fmt.Sprintf(" GH_AW_MISSING_TOOL_LABELS: '%s'\n", escapedJSON)) } } @@ -164,7 +166,9 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa if err != nil { notifyCommentLog.Printf("Warning: failed to serialize messages config for agent failure handler: %v", err) } else if messagesJSON != "" { - agentFailureEnvVars = append(agentFailureEnvVars, fmt.Sprintf(" GH_AW_SAFE_OUTPUT_MESSAGES: %q\n", messagesJSON)) + // Escape single quotes and backslashes for safe embedding in YAML single-quoted strings + escapedJSON := escapeSingleQuote(messagesJSON) + agentFailureEnvVars = append(agentFailureEnvVars, fmt.Sprintf(" GH_AW_SAFE_OUTPUT_MESSAGES: '%s'\n", escapedJSON)) } } @@ -225,7 +229,9 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa if err != nil { notifyCommentLog.Printf("Warning: failed to serialize messages config: %v", err) } else if messagesJSON != "" { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_SAFE_OUTPUT_MESSAGES: %q\n", messagesJSON)) + // Escape single quotes and backslashes for safe embedding in YAML single-quoted strings + escapedJSON := escapeSingleQuote(messagesJSON) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_SAFE_OUTPUT_MESSAGES: '%s'\n", escapedJSON)) } } @@ -233,7 +239,9 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa if len(safeOutputJobNames) > 0 { safeOutputJobsJSON, jobURLEnvVars := buildSafeOutputJobsEnvVars(safeOutputJobNames) if safeOutputJobsJSON != "" { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_SAFE_OUTPUT_JOBS: %q\n", safeOutputJobsJSON)) + // Escape single quotes and backslashes for safe embedding in YAML single-quoted strings + escapedJSON := escapeSingleQuote(safeOutputJobsJSON) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_SAFE_OUTPUT_JOBS: '%s'\n", escapedJSON)) customEnvVars = append(customEnvVars, jobURLEnvVars...) notifyCommentLog.Printf("Added safe output jobs info for %d job(s)", len(safeOutputJobNames)) } diff --git a/pkg/workflow/safe_outputs_env.go b/pkg/workflow/safe_outputs_env.go index 6d362031a7..39aad09e74 100644 --- a/pkg/workflow/safe_outputs_env.go +++ b/pkg/workflow/safe_outputs_env.go @@ -163,7 +163,9 @@ func (c *Compiler) buildStandardSafeOutputEnvVars(data *WorkflowData, targetRepo if err != nil { safeOutputsEnvLog.Printf("Warning: failed to serialize messages config: %v", err) } else if messagesJSON != "" { - customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_SAFE_OUTPUT_MESSAGES: %q\n", messagesJSON)) + // Escape single quotes and backslashes for safe embedding in YAML single-quoted strings + escapedJSON := escapeSingleQuote(messagesJSON) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_SAFE_OUTPUT_MESSAGES: '%s'\n", escapedJSON)) } }