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..395854788c4 100644 --- a/pkg/cli/download_workflow.go +++ b/pkg/cli/download_workflow.go @@ -60,6 +60,17 @@ 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) + } + // 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/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..c22abeb7d21 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. + // 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 exactly "owner/repo" with no dot-only path components. + const safeRepoPattern = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/; + if (!safeRepoPattern.test(repositoryPath) || repositoryPath.split("/").some(p => p === "." || p === "..")) { + 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);