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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ What does this mean for you? It means that strings that contain a lot of nested

Remote-helper overrides (`--upload-pack`, `--receive-pack`, `--exec`, and abbreviations of those) are rejected: they can make git run an arbitrary Git transport program during fetch/pull/push.
Message-from-file flags (`-F`, `--file`, abbreviations such as `--fi`, and short-option clusters that include `F` such as `-aF`) are rejected: they can embed arbitrary runner filesystem contents into a tag or commit message and, with a push, into the repository history.
Do not interpolate untrusted data (for example values from `github.event.*` or repository content that contributors can edit) into `fetch`, `pull`, `push`, `tag_push`, `tag`, or `commit` without sanitizing them first.
Unmatched `'` / `"` quotes are also rejected: `string-argv` can otherwise split on an odd quote and turn part of a value into extra flags (for example a branch name like `fix'--force` becoming `fix` plus `--force`).
Do not interpolate untrusted data (for example values from `github.event.*`, `github.head_ref`, or repository content that contributors can edit) into `fetch`, `pull`, `push`, `tag`, `tag_push`, or `commit` without sanitizing them first. When the branch name is dynamic, prefer the default `push: true` with [`new_branch`](#creating-a-new-branch) instead of embedding the ref in a custom `push` string.

### Adding files

Expand All @@ -126,7 +127,7 @@ By default the action runs the following command: `git push origin ${new_branch
- any other string:
The action will use your string as the arguments for the `git push` command. Please note that nothing is used other than your arguments, and the command will result in `git push ${push input}` (no remote, no branch, no `--set-upstream`, you have to include them yourself).

One way to use this is if you want to force push to a branch of your repo: you'll need to set the `push` input to, for example, `origin yourBranch --force`.
One way to use this is if you want to force push to a trusted/static branch name in your repo: set the `push` input to, for example, `origin yourBranch --force`. Do not build that string from untrusted refs such as `github.head_ref`.

### Creating a new branch

Expand All @@ -135,7 +136,7 @@ If you want the action to commit in a new branch, you can use the `new_branch` i
Please note that if the branch exists, the action will still try push to it, but it's possible that the push will be rejected by the remote as non-straightforward.

If that's the case, you need to make sure that the branch you want to commit to is already checked out before you run the action.
If you're **really** sure that you want to commit to that branch, you can also force-push by setting the `push` input to something like `origin yourBranchName --set-upstream --force`.
If you're **really** sure that you want to commit to that branch, you can also force-push by setting the `push` input to something like `origin yourBranchName --set-upstream --force` (use a trusted/static branch name, not an untrusted ref).

If you want to commit files "across different branches", here are two ways to do it:

Expand Down
2 changes: 1 addition & 1 deletion lib/index.js

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,28 @@ function consumesFollowingArgument(arg: string): boolean {
return false;
}

/**
* Rejects unmatched `'` / `"` so `string-argv` cannot silently retokenize at an
* odd quote (e.g. `origin fix'--force` → `["origin","fix","--force"]`).
* Balanced quotes and the opposite quote type inside a quoted segment are allowed.
*/
function assertBalancedQuotes(input: string): void {
let open: "'" | '"' | null = null;
for (const char of input) {
if (char !== "'" && char !== '"') continue;
if (open === null) {
open = char;
} else if (open === char) {
open = null;
}
}
if (open !== null) {
throw new Error(
`Git arguments contain an unmatched ${open} quote. Unclosed quotes are rejected because they cause ambiguous argument splitting.`,
);
}
}

/**
* Matches the given string to an array of arguments.
* The parsing is made by `string-argv`: if your way of using argument is not supported, the issue is theirs!
Expand All @@ -250,10 +272,13 @@ function consumesFollowingArgument(arg: string): boolean {
* matchGitArgs(' ') => [ ]
* ```
* @returns An array, if there's no match it'll be empty
* @throws If the args include unmatched quotes
* @throws If the args include a blocked remote-helper override (`--upload-pack`, `--receive-pack`, `--exec`, or abbreviations)
* @throws If the args include a blocked message-from-file flag (`-F`, `--file`, abbreviations, or short-option clusters containing `F`)
*/
export function matchGitArgs(string: string) {
assertBalancedQuotes(string);

const parsed = parseArgsStringToArgv(string);
core.debug(`Git args parsed:
- Original: ${string}
Expand Down
24 changes: 24 additions & 0 deletions test/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,30 @@ describe('matchGitArgs', () => {
expect(() => matchGitArgs('--exe=evil')).toThrow(/not allowed/);
});

it('rejects unmatched quotes that would inject flags via string-argv', () => {
expect(() => matchGitArgs("origin fix'--force --set-upstream")).toThrow(
/unmatched ' quote/,
);
expect(() => matchGitArgs('origin fix"--force --set-upstream')).toThrow(
/unmatched " quote/,
);
});

it('parses balanced quotes without treating them as injection', () => {
expect(matchGitArgs("origin a'b'c --set-upstream")).toStrictEqual([
'origin',
"a'b'c",
'--set-upstream',
]);
expect(matchGitArgs("--longOption 'hello world'")).toStrictEqual([
'--longOption',
'hello world',
]);
expect(
matchGitArgs('--longOption \'This uses the "other" quotes\''),
).toStrictEqual(['--longOption', 'This uses the "other" quotes']);
});

it('rejects -F / --file message-from-file flags (PoC form)', () => {
expect(() =>
matchGitArgs('1.0.0 -F ../runner-secrets/aws-credentials.txt'),
Expand Down