Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion pkg/cli/bootstrap_profile_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,11 @@ func buildBootstrapGitHubAppRegistrationURL(owner, ownerType, state string) stri

const bootstrapRegistrationPageTmpl = `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Redirecting To GitHub App Creation</title></head><body><p>Redirecting to GitHub App creation...</p><form id="manifest-form" action="{{.Action}}" method="post"><input type="hidden" name="manifest" value="{{.Manifest}}"><noscript><button type="submit">Continue To GitHub App Creation</button></noscript></form><script>document.getElementById('manifest-form').submit();</script></body></html>`

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)
Expand Down
11 changes: 11 additions & 0 deletions pkg/cli/download_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/cli/logs_format_compact.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions scripts/ensure-docs-slide-pdf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
4 changes: 2 additions & 2 deletions scripts/generate-agent-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);

Expand All @@ -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);
Expand Down
Loading