diff --git a/src/commitSha.ts b/src/commitSha.ts index c48ba8e..2fd8aa4 100644 --- a/src/commitSha.ts +++ b/src/commitSha.ts @@ -3,6 +3,7 @@ import * as github from '@actions/github' import {Env} from './env' import {Inputs} from './inputs' +import { isSafeGitArg } from './utils' import { canDiffCommits, cleanShaInput, @@ -343,6 +344,31 @@ export const getSHAForPullRequestEvent = async ({ }: SHAForPullRequestEvent): Promise => { let targetBranch = github.context.payload.pull_request?.base?.ref const currentBranch = github.context.payload.pull_request?.head?.ref + + // Validate branch/ref names and any gitFetchExtraArgs that may be tainted + import { isSafeGitArg } from './utils' + const argListToValidate = [ + ...(Array.isArray(gitFetchExtraArgs) ? gitFetchExtraArgs : []), + remoteName + ] + if (currentBranch && !isSafeGitArg(currentBranch)) { + throw new Error( + `Unsafe branch name detected in PR head.ref: "${currentBranch}". Possible injection attempt.` + ) + } + if (targetBranch && !isSafeGitArg(targetBranch)) { + throw new Error( + `Unsafe branch name detected in PR base.ref: "${targetBranch}". Possible injection attempt.` + ) + } + for (const a of argListToValidate) { + if (!isSafeGitArg(a)) { + throw new Error( + `Unsafe extra git argument detected: "${a}". Possible injection attempt.` + ) + } + } + if (inputs.sinceLastRemoteCommit) { targetBranch = currentBranch } diff --git a/src/utils.ts b/src/utils.ts index 5ca3bde..0f980ad 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -284,6 +284,28 @@ export const submoduleExists = async ({ return stdout.trim() !== '' } +/** + * Checks if a string is safe to use as a git argument (prevents option injection) + * @param val - the argument to check + * @returns true if safe, false otherwise + */ +export function isSafeGitArg(val: string): boolean { + // Disallow args that start with dash, are empty, or obviously suspicious + if (!val || typeof val !== 'string') return false + if (val.startsWith('-')) return false + // Disallow sequences forbidden in git refs: spaces, control chars, "..", ".lock", etc. + if (/[ \t\n\r]/.test(val)) return false + if (val.includes('..')) return false + if (val.endsWith('.lock')) return false + if (val.includes('@{')) return false + if (val.includes('\\')) return false + if (val.startsWith('/')) return false + if (val.endsWith('/')) return false + if (val.includes('//')) return false + if (val.match(/[\x00-\x1F\x7F]/)) return false + return true +} + /** * Fetches the git repository * @param args - arguments for fetch command