Skip to content

Commit 411a60b

Browse files
dirgimkasemAlem
authored andcommitted
fix: resolve issue with parsing console log URL
* Prevent injection by using simple string replacement instead of template parsing Assisted-by: Cursor AI Signed-off-by: dirgim <kpavic@redhat.com> rh-pre-commit.version: 2.2.0 rh-pre-commit.check-secrets: ENABLED
1 parent 9b742d0 commit 411a60b

2 files changed

Lines changed: 56 additions & 26 deletions

File tree

status/format.go

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,6 @@ type SummaryTemplateData struct {
8787
Logger logr.Logger
8888
}
8989

90-
// TaskLogTemplateData holds the data necessary to construct a Task log URL.
91-
type TaskLogTemplateData struct {
92-
TaskName string
93-
PipelineRunName string
94-
Namespace string
95-
}
96-
9790
// CommentTemplateData holds the data necessary to construct a PipelineRun comment.
9891
type CommentTemplateData struct {
9992
Title string
@@ -296,19 +289,43 @@ func FormatFootnotes(taskRuns []*helpers.TaskRun) (string, error) {
296289
return strings.Join(footnotes, "\n"), nil
297290
}
298291

292+
// Console URL env vars (CONSOLE_URL, CONSOLE_URL_TASKLOG) use literal placeholder substitution only.
293+
// Operator-controlled values are not evaluated as Go templates (avoids CWE-94 / server-side template injection).
294+
//
295+
// Supported placeholders:
296+
//
297+
// {{NAMESPACE}} — Kubernetes namespace
298+
// {{PIPELINE_RUN_NAME}} — PipelineRun name
299+
// {{TASK_NAME}} — Pipeline task name (CONSOLE_URL_TASKLOG only; empty for CONSOLE_URL)
300+
//
301+
// Legacy placeholders (backward compatible with earlier releases):
302+
//
303+
// {{ .Namespace }}, {{ .PipelineRunName }}, {{ .TaskName }}
304+
func substituteConsoleURLPlaceholders(pattern string, namespace, pipelineRunName, taskName string) string {
305+
// Support both documented placeholders and legacy Go-template-ish forms.
306+
legacyAndDocumented := strings.NewReplacer(
307+
// Legacy forms with/without spaces around field names
308+
"{{ .Namespace }}", namespace,
309+
"{{.Namespace}}", namespace,
310+
"{{ .PipelineRunName }}", pipelineRunName,
311+
"{{.PipelineRunName}}", pipelineRunName,
312+
"{{ .TaskName }}", taskName,
313+
"{{.TaskName}}", taskName,
314+
// Documented placeholders
315+
"{{NAMESPACE}}", namespace,
316+
"{{PIPELINE_RUN_NAME}}", pipelineRunName,
317+
"{{TASK_NAME}}", taskName,
318+
)
319+
return legacyAndDocumented.Replace(pattern)
320+
}
321+
299322
// FormatPipelineURL accepts a name of application, pipelinerun, namespace and returns a complete pipelineURL.
300-
func FormatPipelineURL(pipelinerun string, namespace string, logger logr.Logger) string {
301-
console_url := os.Getenv("CONSOLE_URL")
302-
if console_url == "" {
323+
func FormatPipelineURL(pipelinerun string, namespace string, _ logr.Logger) string {
324+
consoleURL := os.Getenv("CONSOLE_URL")
325+
if consoleURL == "" {
303326
return "https://CONSOLE_URL_NOT_AVAILABLE"
304327
}
305-
buf := bytes.Buffer{}
306-
data := SummaryTemplateData{PipelineRunName: pipelinerun, Namespace: namespace}
307-
t := template.Must(template.New("").Parse(console_url))
308-
if err := t.Execute(&buf, data); err != nil {
309-
logger.Error(err, "Error occured when executing template.")
310-
}
311-
return buf.String()
328+
return substituteConsoleURLPlaceholders(consoleURL, namespace, pipelinerun, "")
312329
}
313330

314331
// FormatPullRequestURL accepts a name of application, pipelinerun, namespace and returns a complete pipelineURL.
@@ -333,18 +350,11 @@ func FormatRepoURL(repoUrl string) string {
333350
}
334351

335352
// FormatTaskLogURL accepts name of pipelinerun, task, namespace and returns a complete task log URL.
336-
func FormatTaskLogURL(taskRun *helpers.TaskRun, pipelinerun string, namespace string, logger logr.Logger) string {
353+
func FormatTaskLogURL(taskRun *helpers.TaskRun, pipelinerun string, namespace string, _ logr.Logger) string {
337354
consoleTaskLogURL := os.Getenv("CONSOLE_URL_TASKLOG")
338355
if consoleTaskLogURL == "" {
339356
return "https://CONSOLE_URL_TASKLOG_NOT_AVAILABLE"
340357
}
341-
342358
taskName := taskRun.GetPipelineTaskName()
343-
buf := bytes.Buffer{}
344-
data := TaskLogTemplateData{PipelineRunName: pipelinerun, TaskName: taskName, Namespace: namespace}
345-
t := template.Must(template.New("").Parse(consoleTaskLogURL))
346-
if err := t.Execute(&buf, data); err != nil {
347-
logger.Error(err, "Error occured when executing task log template.")
348-
}
349-
return buf.String()
359+
return substituteConsoleURLPlaceholders(consoleTaskLogURL, namespace, pipelinerun, taskName)
350360
}

status/format_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,26 @@ var _ = Describe("Formatters", func() {
271271
Expect(taskLogUrl).To(Equal(expectedTaskLogURL))
272272
})
273273

274+
It("can construct console URLs using documented placeholders (not Go templates)", func() {
275+
os.Setenv("CONSOLE_URL", "https://example.com/ns/{{NAMESPACE}}/pipelinerun/{{PIPELINE_RUN_NAME}}")
276+
os.Setenv("CONSOLE_URL_TASKLOG", "https://example.com/ns/{{NAMESPACE}}/pipelinerun/{{PIPELINE_RUN_NAME}}/logs/{{TASK_NAME}}")
277+
taskLogURL := status.FormatTaskLogURL(taskRuns[0], pipelineRun.Name, pipelineRun.Namespace, logr.Discard())
278+
Expect(taskLogURL).To(Equal("https://example.com/ns/default/pipelinerun/pipelinerun-component-sample/logs/example-task-1"))
279+
summary, err := status.FormatTestsSummary(taskRuns, pipelineRun.Name, pipelineRun.Namespace, componentSnapshotInfos, PRGroup, logr.Discard())
280+
Expect(err).ToNot(HaveOccurred())
281+
Expect(summary).To(ContainSubstring(`href="https://example.com/ns/default/pipelinerun/pipelinerun-component-sample"`))
282+
})
283+
284+
It("supports compact legacy placeholders without spaces", func() {
285+
os.Setenv("CONSOLE_URL", "https://example.com/ns/{{.Namespace}}/pipelinerun/{{.PipelineRunName}}")
286+
os.Setenv("CONSOLE_URL_TASKLOG", "https://example.com/ns/{{.Namespace}}/pipelinerun/{{.PipelineRunName}}/logs/{{.TaskName}}")
287+
taskLogURL := status.FormatTaskLogURL(taskRuns[0], pipelineRun.Name, pipelineRun.Namespace, logr.Discard())
288+
Expect(taskLogURL).To(Equal("https://example.com/ns/default/pipelinerun/pipelinerun-component-sample/logs/example-task-1"))
289+
summary, err := status.FormatTestsSummary(taskRuns, pipelineRun.Name, pipelineRun.Namespace, componentSnapshotInfos, PRGroup, logr.Discard())
290+
Expect(err).ToNot(HaveOccurred())
291+
Expect(summary).To(ContainSubstring(`href="https://example.com/ns/default/pipelinerun/pipelinerun-component-sample"`))
292+
})
293+
274294
It("can construct a summary", func() {
275295
summary, err := status.FormatTestsSummary(taskRuns, pipelineRun.Name, pipelineRun.Namespace, componentSnapshotInfos, PRGroup, logr.Discard())
276296
Expect(err).ToNot(HaveOccurred())

0 commit comments

Comments
 (0)