mirror of
https://github.com/semantic-release/semantic-release.git
synced 2026-10-25 07:29:35 +00:00
fix: argument Injection via repositoryUrl in package.json (#4245)
Co-Authored-By: Abhijith S abh78i00@gmail.com
This commit is contained in:
Vendored
+3
@@ -627,6 +627,9 @@ declare module "semantic-release" {
|
||||
* Any valid git url format is supported (see
|
||||
* [git protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols))
|
||||
*
|
||||
* The value must not begin with `-`, as it would otherwise be interpreted
|
||||
* by `git` as a command-line option rather than a URL.
|
||||
*
|
||||
* Default: `repository` property in `package.json`, or git origin url.
|
||||
*/
|
||||
repositoryUrl?: string | undefined;
|
||||
|
||||
@@ -42,6 +42,21 @@ Please make sure to add the \`repositoryUrl\` to the [semantic-release configura
|
||||
};
|
||||
}
|
||||
|
||||
export function EINVALIDREPOURL({ repositoryUrl }) {
|
||||
return {
|
||||
message: "The `repositoryUrl` option is not a valid git URL.",
|
||||
details: `The [repositoryUrl option](${linkify(
|
||||
"usage/configuration#repositoryurl"
|
||||
)}) must be a valid git URL and cannot begin with \`-\`, as \`git\` would otherwise interpret it as a command-line option rather than a URL.
|
||||
|
||||
The invalid \`repositoryUrl\` is \`${repositoryUrl}\`.
|
||||
|
||||
Please verify the \`repository\` property in your \`package.json\` and the [repositoryUrl option](${linkify(
|
||||
"usage/configuration#repositoryurl"
|
||||
)}) in your [semantic-release configuration](${linkify("usage/configuration#configuration")}).`,
|
||||
};
|
||||
}
|
||||
|
||||
export function EGITNOPERMISSION({ options: { repositoryUrl }, branch: { name } }) {
|
||||
return {
|
||||
message: "Cannot push to the Git repository.",
|
||||
|
||||
+13
-11
@@ -67,7 +67,7 @@ export async function getCommits(from, to, execaOptions) {
|
||||
* @throws {Error} If the `git` command fails.
|
||||
*/
|
||||
export async function getBranches(repositoryUrl, execaOptions) {
|
||||
return (await execa("git", ["ls-remote", "--heads", repositoryUrl], execaOptions)).stdout
|
||||
return (await execa("git", ["ls-remote", "--heads", "--", repositoryUrl], execaOptions)).stdout
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((branch) => branch.match(/^.+refs\/heads\/(?<branch>.+)$/)[1]);
|
||||
@@ -115,8 +115,8 @@ export async function fetch(repositoryUrl, branch, ciBranch, execaOptions) {
|
||||
"--unshallow",
|
||||
"--tags",
|
||||
...(branch === ciBranch && !isDetachedHead
|
||||
? [repositoryUrl]
|
||||
: ["--update-head-ok", repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]),
|
||||
? ["--", repositoryUrl]
|
||||
: ["--update-head-ok", "--", repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]),
|
||||
],
|
||||
execaOptions
|
||||
);
|
||||
@@ -127,8 +127,8 @@ export async function fetch(repositoryUrl, branch, ciBranch, execaOptions) {
|
||||
"fetch",
|
||||
"--tags",
|
||||
...(branch === ciBranch && !isDetachedHead
|
||||
? [repositoryUrl]
|
||||
: ["--update-head-ok", repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]),
|
||||
? ["--", repositoryUrl]
|
||||
: ["--update-head-ok", "--", repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]),
|
||||
],
|
||||
execaOptions
|
||||
);
|
||||
@@ -143,9 +143,9 @@ export async function fetch(repositoryUrl, branch, ciBranch, execaOptions) {
|
||||
*/
|
||||
export async function fetchNotes(repositoryUrl, execaOptions) {
|
||||
try {
|
||||
await execa("git", ["fetch", "--unshallow", repositoryUrl, `+refs/notes/*:refs/notes/*`], execaOptions);
|
||||
await execa("git", ["fetch", "--unshallow", "--", repositoryUrl, `+refs/notes/*:refs/notes/*`], execaOptions);
|
||||
} catch {
|
||||
await execa("git", ["fetch", repositoryUrl, `+refs/notes/*:refs/notes/*`], {
|
||||
await execa("git", ["fetch", "--", repositoryUrl, `+refs/notes/*:refs/notes/*`], {
|
||||
...execaOptions,
|
||||
reject: false,
|
||||
});
|
||||
@@ -204,7 +204,7 @@ export async function isGitRepo(execaOptions) {
|
||||
*/
|
||||
export async function verifyAuth(repositoryUrl, branch, execaOptions) {
|
||||
try {
|
||||
await execa("git", ["push", "--dry-run", "--no-verify", repositoryUrl, `HEAD:${branch}`], execaOptions);
|
||||
await execa("git", ["push", "--dry-run", "--no-verify", "--", repositoryUrl, `HEAD:${branch}`], execaOptions);
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
throw error;
|
||||
@@ -233,7 +233,7 @@ export async function tag(tagName, ref, execaOptions) {
|
||||
* @throws {Error} if the push failed.
|
||||
*/
|
||||
export async function push(repositoryUrl, execaOptions) {
|
||||
await execa("git", ["push", "--tags", repositoryUrl], execaOptions);
|
||||
await execa("git", ["push", "--tags", "--", repositoryUrl], execaOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,7 +245,7 @@ export async function push(repositoryUrl, execaOptions) {
|
||||
* @throws {Error} if the push failed.
|
||||
*/
|
||||
export async function pushNotes(repositoryUrl, ref, execaOptions) {
|
||||
await execa("git", ["push", repositoryUrl, `refs/notes/${GIT_NOTE_REF}-${ref}`], execaOptions);
|
||||
await execa("git", ["push", "--", repositoryUrl, `refs/notes/${GIT_NOTE_REF}-${ref}`], execaOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,7 +292,9 @@ export async function verifyBranchName(branch, execaOptions) {
|
||||
export async function isBranchUpToDate(repositoryUrl, branch, execaOptions) {
|
||||
return (
|
||||
(await getGitHead(execaOptions)) ===
|
||||
(await execa("git", ["ls-remote", "--heads", repositoryUrl, branch], execaOptions)).stdout.match(/^(?<ref>\w+)?/)[1]
|
||||
(await execa("git", ["ls-remote", "--heads", "--", repositoryUrl, branch], execaOptions)).stdout.match(
|
||||
/^(?<ref>\w+)?/
|
||||
)[1]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ export default async (context) => {
|
||||
errors.push(getError("ENOGITREPO", { cwd }));
|
||||
} else if (!repositoryUrl) {
|
||||
errors.push(getError("ENOREPOURL"));
|
||||
} else if (repositoryUrl.startsWith("-")) {
|
||||
// A `repositoryUrl` beginning with `-` would be interpreted by `git` as a
|
||||
// command-line option rather than a URL, enabling argument injection.
|
||||
errors.push(getError("EINVALIDREPOURL", { repositoryUrl }));
|
||||
}
|
||||
|
||||
// Verify that compiling the `tagFormat` produce a valid Git tag
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "ava";
|
||||
import { temporaryDirectory } from "tempy";
|
||||
import {
|
||||
@@ -423,3 +425,30 @@ test("Fetch all notes on a detached head repository", async (t) => {
|
||||
|
||||
t.is(await gitGetNote(commit.hash, { cwd }), '{"note":"note"}');
|
||||
});
|
||||
|
||||
test("Does not execute a `repositoryUrl` injected as an `--upload-pack` git option", async (t) => {
|
||||
const { cwd } = await gitRepo();
|
||||
await gitCommits(["First"], { cwd });
|
||||
const marker = path.join(temporaryDirectory(), "upload-pack-rce");
|
||||
// A `repositoryUrl` beginning with `-` would be parsed by git as a
|
||||
// command-line option rather than a positional argument. `--upload-pack`
|
||||
// lets the injected value run an arbitrary binary when fetching refs.
|
||||
const repositoryUrl = `--upload-pack=touch ${marker}`;
|
||||
|
||||
await t.throwsAsync(isBranchUpToDate(repositoryUrl, "master", { cwd }));
|
||||
|
||||
t.false(existsSync(marker));
|
||||
});
|
||||
|
||||
test("Does not execute a `repositoryUrl` injected as a `--receive-pack` git option", async (t) => {
|
||||
const { cwd } = await gitRepo(true);
|
||||
await gitCommits(["First"], { cwd });
|
||||
const marker = path.join(temporaryDirectory(), "receive-pack-rce");
|
||||
// `--receive-pack` is the push-side equivalent: it lets the injected value
|
||||
// run an arbitrary binary when pushing to the (configured) remote.
|
||||
const repositoryUrl = `--receive-pack=touch ${marker}`;
|
||||
|
||||
await t.throwsAsync(push(repositoryUrl, { cwd }));
|
||||
|
||||
t.false(existsSync(marker));
|
||||
});
|
||||
|
||||
@@ -109,6 +109,18 @@ test("Throw a SemanticReleaseError for each invalid branch", async (t) => {
|
||||
t.truthy(errors[5].details);
|
||||
});
|
||||
|
||||
test('Throw a SemanticReleaseError if the "repositoryUrl" starts with "-"', async (t) => {
|
||||
const { cwd } = await gitRepo(true);
|
||||
const options = { repositoryUrl: "--upload-pack=/bin/sh", tagFormat: `v\${version}`, branches: [{ name: "master" }] };
|
||||
|
||||
const errors = [...(await t.throwsAsync(verify({ cwd, options }))).errors];
|
||||
|
||||
t.is(errors[0].name, "SemanticReleaseError");
|
||||
t.is(errors[0].code, "EINVALIDREPOURL");
|
||||
t.truthy(errors[0].message);
|
||||
t.truthy(errors[0].details);
|
||||
});
|
||||
|
||||
test('Return "true" if all verification pass', async (t) => {
|
||||
const { cwd, repositoryUrl } = await gitRepo(true);
|
||||
const options = { repositoryUrl, tagFormat: `v\${version}`, branches: [{ name: "master" }] };
|
||||
|
||||
Reference in New Issue
Block a user