From dd07bd01dc9de8066a91e2c03934ea88dd76549a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:32:57 +0000 Subject: [PATCH 1/3] Initial plan From 659536c60b0f99043ef8b5c4a64dd93a17a07f10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:49:12 +0000 Subject: [PATCH 2/3] fix: address sighthound security findings (path traversal, SSRF, false positives) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/bootstrap_profile_helpers.go | 6 +++++- pkg/cli/download_workflow.go | 8 ++++++++ pkg/cli/logs_format_compact.go | 3 +++ scripts/ensure-docs-slide-pdf.js | 14 ++++++++++++++ scripts/generate-agent-factory.js | 4 ++-- 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/pkg/cli/bootstrap_profile_helpers.go b/pkg/cli/bootstrap_profile_helpers.go index 1af2099c04b..fef56146fea 100644 --- a/pkg/cli/bootstrap_profile_helpers.go +++ b/pkg/cli/bootstrap_profile_helpers.go @@ -255,7 +255,11 @@ func buildBootstrapGitHubAppRegistrationURL(owner, ownerType, state string) stri const bootstrapRegistrationPageTmpl = `Redirecting To GitHub App Creation

Redirecting to GitHub App creation...

` -var bootstrapRegistrationPage = template.Must(template.New("bootstrap").Parse(bootstrapRegistrationPageTmpl)) +// bootstrapRegistrationPage is a pre-compiled html/template whose source is a +// package-level constant (bootstrapRegistrationPageTmpl above). Dynamic values +// are passed only as template data — never as template source — so SSTI is not +// possible. The html/template package auto-escapes all interpolated values. +var bootstrapRegistrationPage = template.Must(template.New("bootstrap").Parse(bootstrapRegistrationPageTmpl)) // #nosec G203 func renderBootstrapGitHubAppRegistrationPage(registrationURL string, manifest map[string]any) (string, error) { encoded, err := json.Marshal(manifest) diff --git a/pkg/cli/download_workflow.go b/pkg/cli/download_workflow.go index 19d5b74da34..243212c224d 100644 --- a/pkg/cli/download_workflow.go +++ b/pkg/cli/download_workflow.go @@ -60,6 +60,14 @@ func downloadWorkflowContentViaGit(ctx context.Context, repo, path, ref string, // downloadWorkflowContentViaGitClone downloads a workflow file by shallow cloning with sparse checkout func downloadWorkflowContentViaGitClone(ctx context.Context, repo, path, ref string, verbose bool) ([]byte, error) { + // Validate and clean the path early to prevent path traversal before it is + // used as a sparse-checkout pattern or joined with a filesystem base directory. + cleanedPath := filepath.Clean(path) + if filepath.IsAbs(cleanedPath) || strings.HasPrefix(cleanedPath, ".."+string(filepath.Separator)) || cleanedPath == ".." { + return nil, fmt.Errorf("unsafe path in workflow reference: %q", path) + } + path = cleanedPath + if verbose { fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Fetching %s/%s@%s via git clone", repo, path, ref))) } diff --git a/pkg/cli/logs_format_compact.go b/pkg/cli/logs_format_compact.go index 28a32b3ad53..ae0dac8f09d 100644 --- a/pkg/cli/logs_format_compact.go +++ b/pkg/cli/logs_format_compact.go @@ -46,6 +46,9 @@ func workflowIDFromRun(path, name string) string { // consumption to w. Designed for LLM context windows: minimal formatting, no decoration, // structured but flat. // +// NOTE: w is always a plain-text writer (os.Stdout or a bytes.Buffer in tests). +// This function is never wired to an HTTP response, so HTML escaping is not applicable. +// // Format sections: // // [summary] key=value pairs on one line diff --git a/scripts/ensure-docs-slide-pdf.js b/scripts/ensure-docs-slide-pdf.js index 05f350dee28..b0d90982d6f 100644 --- a/scripts/ensure-docs-slide-pdf.js +++ b/scripts/ensure-docs-slide-pdf.js @@ -102,6 +102,20 @@ async function readPdfBytes() { const ref = getGitRef(); const repositoryPath = getRepositoryPath(); + + // Validate each URL component before interpolating into the request URL. + // A SHA is 40 hex characters; branch/tag names may contain alphanumerics, + // hyphens, underscores, dots, and forward-slashes (for namespaced refs). + const safeRefPattern = /^[a-zA-Z0-9_.\-/]+$/; + if (!safeRefPattern.test(ref)) { + throw new Error(`Unsafe git ref value: ${ref}`); + } + // Repository path must be "owner/repo" with no additional path components. + const safeRepoPattern = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/; + if (!safeRepoPattern.test(repositoryPath)) { + throw new Error(`Unsafe repository path value: ${repositoryPath}`); + } + const url = `https://media.githubusercontent.com/media/${repositoryPath}/${ref}/docs/slides/github-agentic-workflows.pdf`; console.warn(`Detected Git LFS pointer at ${SOURCE_PATH}; downloading ${url}`); diff --git a/scripts/generate-agent-factory.js b/scripts/generate-agent-factory.js index c43c1af042d..0cdcfd44783 100755 --- a/scripts/generate-agent-factory.js +++ b/scripts/generate-agent-factory.js @@ -214,7 +214,7 @@ console.log("Generating agent factory documentation..."); const lockFiles = fs .readdirSync(WORKFLOWS_DIR) .filter(file => file.endsWith(".lock.yml")) - .map(file => path.join(WORKFLOWS_DIR, file)); + .map(file => path.join(WORKFLOWS_DIR, path.basename(file))); console.log(`Found ${lockFiles.length} lock files`); @@ -229,7 +229,7 @@ const workflows = lockFiles // Try to find corresponding .md file // Convert "workflow-name.lock.yml" to "workflow-name.md" const mdFilename = workflowInfo.filename.replace(".lock.yml", ".md"); - const mdFilePath = path.join(WORKFLOWS_DIR, mdFilename); + const mdFilePath = path.join(WORKFLOWS_DIR, path.basename(mdFilename)); // Extract all workflow metadata from markdown file const engine = extractEngineFromMarkdown(mdFilePath); From d20fb6b0e364a345b0e642338243f91da3745867 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:09:46 +0000 Subject: [PATCH 3/3] fix: tighten security validations per review feedback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/download_workflow.go | 5 ++++- scripts/ensure-docs-slide-pdf.js | 12 ++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pkg/cli/download_workflow.go b/pkg/cli/download_workflow.go index 243212c224d..395854788c4 100644 --- a/pkg/cli/download_workflow.go +++ b/pkg/cli/download_workflow.go @@ -66,7 +66,10 @@ func downloadWorkflowContentViaGitClone(ctx context.Context, repo, path, ref str if filepath.IsAbs(cleanedPath) || strings.HasPrefix(cleanedPath, ".."+string(filepath.Separator)) || cleanedPath == ".." { return nil, fmt.Errorf("unsafe path in workflow reference: %q", path) } - path = cleanedPath + // Use forward slashes so the sparse-checkout pattern is valid for Git on all + // platforms (Git patterns use "/" as separator; backslashes are escapes). + // filepath.Join handles forward slashes correctly when building the local path. + path = filepath.ToSlash(cleanedPath) if verbose { fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Fetching %s/%s@%s via git clone", repo, path, ref))) diff --git a/scripts/ensure-docs-slide-pdf.js b/scripts/ensure-docs-slide-pdf.js index b0d90982d6f..c22abeb7d21 100644 --- a/scripts/ensure-docs-slide-pdf.js +++ b/scripts/ensure-docs-slide-pdf.js @@ -104,15 +104,15 @@ async function readPdfBytes() { const repositoryPath = getRepositoryPath(); // Validate each URL component before interpolating into the request URL. - // A SHA is 40 hex characters; branch/tag names may contain alphanumerics, - // hyphens, underscores, dots, and forward-slashes (for namespaced refs). - const safeRefPattern = /^[a-zA-Z0-9_.\-/]+$/; - if (!safeRefPattern.test(ref)) { + // getGitRef() always returns a 40-character hex commit SHA (from GITHUB_SHA + // or `git rev-parse HEAD`). + const safeSHAPattern = /^[0-9a-f]{40}$/i; + if (!safeSHAPattern.test(ref)) { throw new Error(`Unsafe git ref value: ${ref}`); } - // Repository path must be "owner/repo" with no additional path components. + // Repository path must be exactly "owner/repo" with no dot-only path components. const safeRepoPattern = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/; - if (!safeRepoPattern.test(repositoryPath)) { + if (!safeRepoPattern.test(repositoryPath) || repositoryPath.split("/").some(p => p === "." || p === "..")) { throw new Error(`Unsafe repository path value: ${repositoryPath}`); }