From 2f0d5064e7af30b35be377d42f2f1a5e33e200ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Tue, 31 Mar 2026 12:27:32 +0200 Subject: [PATCH 1/3] Adds approvedGitRepositories --- .../sources/protocols/git.test.ts | 40 +++++++++++++++++++ .../01-general-reference/protocols/git.mdx | 10 +++++ .../static/configuration/yarnrc.json | 11 +++++ packages/plugin-git/sources/gitUtils.ts | 12 +++++- packages/plugin-git/sources/index.ts | 7 ++++ 5 files changed, 79 insertions(+), 1 deletion(-) diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/protocols/git.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/protocols/git.test.ts index 4a5529d151f3..5b7f6e0d587a 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/protocols/git.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/protocols/git.test.ts @@ -21,6 +21,14 @@ const TESTED_URLS = { [`https://github.com/yarnpkg/util-deprecate.git#b3562c2798507869edb767da869cd7b85487726d`]: {version: `1.0.0`, runOnCI: true}, }; +const defaultGitConfiguration = { + approvedGitRepositories: [ + `http://localhost:*/repositories/*.git`, + `https://github.com/yarnpkg/util-deprecate.git`, + `ssh://git@github.com/yarnpkg/util-deprecate.git`, + ], +}; + describe(`Protocols`, () => { describe(`git:`, () => { for (const [url, {version, runOnCI}] of Object.entries(TESTED_URLS)) { @@ -34,6 +42,7 @@ describe(`Protocols`, () => { { dependencies: {[`util-deprecate`]: url}, }, + defaultGitConfiguration, async ({path, run, source}) => { await run(`install`); @@ -54,6 +63,7 @@ describe(`Protocols`, () => { [`has-prepack`]: tests.startPackageServer().then(url => `${url}/repositories/has-prepack.git`), }, }, + defaultGitConfiguration, async ({path, run, source}) => { await run(`install`); @@ -70,6 +80,7 @@ describe(`Protocols`, () => { [`no-prepack`]: tests.startPackageServer().then(url => `${url}/repositories/no-prepack.git`), }, }, + defaultGitConfiguration, async ({path, run, source}) => { await run(`install`); @@ -87,6 +98,7 @@ describe(`Protocols`, () => { [`pkg-b`]: tests.startPackageServer().then(url => `${url}/repositories/deep-projects.git#cwd=projects/pkg-b`), }, }, + defaultGitConfiguration, async ({path, run, source}) => { await run(`install`); @@ -103,6 +115,25 @@ describe(`Protocols`, () => { ), ); + test( + `it should block git dependencies from repositories that aren't approved`, + makeTemporaryEnv( + { + dependencies: { + [`has-prepack`]: tests.startPackageServer().then(url => `${url}/repositories/has-prepack.git`), + }, + }, + { + approvedGitRepositories: [`https://github.com/yarnpkg/*`], + }, + async ({run}) => { + await expect(run(`install`)).rejects.toThrow( + /doesn't match any of the patterns in 'approvedGitRepositories'/, + ); + }, + ), + ); + test( `it should support installing workspace packages from projects in subfolders`, makeTemporaryEnv( @@ -112,6 +143,7 @@ describe(`Protocols`, () => { [`lib-b`]: tests.startPackageServer().then(url => `${url}/repositories/deep-projects.git#cwd=projects/pkg-b&workspace=lib`), }, }, + defaultGitConfiguration, async ({path, run, source}) => { await run(`install`); @@ -140,6 +172,7 @@ describe(`Protocols`, () => { [`pkg-b`]: tests.startPackageServer().then(url => `${url}/repositories/workspaces.git#workspace=pkg-b`), }, }, + defaultGitConfiguration, async ({path, run, source}) => { await run(`install`); @@ -164,6 +197,7 @@ describe(`Protocols`, () => { [`yarn-1-project`]: tests.startPackageServer().then(url => `${url}/repositories/yarn-1-project.git`), }, }, + defaultGitConfiguration, async ({path, run, source}) => { await expect(run(`install`, { env: { @@ -188,6 +222,7 @@ describe(`Protocols`, () => { [`npm-project`]: tests.startPackageServer().then(url => `${url}/repositories/npm-project.git`), }, }, + defaultGitConfiguration, async ({path, run, source}) => { await run(`install`); @@ -204,6 +239,7 @@ describe(`Protocols`, () => { [`npm-has-prepack`]: tests.startPackageServer().then(url => `${url}/repositories/npm-has-prepack.git`), }, }, + defaultGitConfiguration, async ({path, run, source}) => { await expect(run(`install`, { env: { @@ -236,6 +272,7 @@ describe(`Protocols`, () => { [`pkg-b`]: tests.startPackageServer().then(url => `${url}/repositories/npm-workspaces.git#workspace=pkg-b`), }, }, + defaultGitConfiguration, async ({path, run, source}) => { const {code, stdout, stderr} = await execUtils.execvp(`npm`, [`--version`], {cwd: path}); if (code !== 0) @@ -271,6 +308,7 @@ describe(`Protocols`, () => { [`yarn-1-project`]: tests.startPackageServer().then(url => `${url}/repositories/yarn-1-project.git`), }, }, + defaultGitConfiguration, async ({path, run, source}) => { // This checks that the `set version classic` part of `scriptUtils.prepareExternalProject` doesn't use Corepack. // The rest of the install will fail though. @@ -295,6 +333,7 @@ describe(`Protocols`, () => { [`no-lockfile-project`]: tests.startPackageServer().then(url => `${url}/repositories/no-lockfile-project.git`), }, }, + defaultGitConfiguration, async ({path, run, source}) => { await expect(run(`install`, { env: { @@ -314,6 +353,7 @@ describe(`Protocols`, () => { [`yarn-1-project`]: tests.startPackageServer().then(url => `${url}/repositories/yarn-1-project.git`), }, }, + defaultGitConfiguration, async ({path, run, source}) => { await expect(run(`install`)).resolves.toBeTruthy(); diff --git a/packages/docusaurus/docs/advanced/01-general-reference/protocols/git.mdx b/packages/docusaurus/docs/advanced/01-general-reference/protocols/git.mdx index 9edc1fe9f28b..ee27d98fdbce 100644 --- a/packages/docusaurus/docs/advanced/01-general-reference/protocols/git.mdx +++ b/packages/docusaurus/docs/advanced/01-general-reference/protocols/git.mdx @@ -11,6 +11,16 @@ The `git:` protocol fetches packages directly from a git repository. This is use yarn add typanion@git@github.com/arcanis/typanion.git ``` +## Repository approval + +Git dependencies are restricted through the `approvedGitRepositories` setting. GitHub repositories must match at least one of its glob patterns, otherwise Yarn will refuse to fetch them. + +```yaml +approvedGitRepositories: + - https://github.com/yarnpkg/* + - ssh://git@github.com/yarnpkg/* +``` + ## Packing The target repository won't be used as-is - it will first be packed using [`pack`](/cli/pack). diff --git a/packages/docusaurus/static/configuration/yarnrc.json b/packages/docusaurus/static/configuration/yarnrc.json index d02418f5ce1b..3eafc2f66ecf 100644 --- a/packages/docusaurus/static/configuration/yarnrc.json +++ b/packages/docusaurus/static/configuration/yarnrc.json @@ -71,6 +71,17 @@ "type": "number", "default": 2 }, + "approvedGitRepositories": { + "_package": "@yarnpkg/plugin-git", + "title": "Array of git repository URL glob patterns that are allowed to be fetched.", + "description": "When set, Yarn will block any git dependency whose normalized repository URL doesn't match one of these patterns. GitHub repositories must be explicitly approved.", + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "_exampleItems": ["https://github.com/yarnpkg/*", "ssh://git@github.com/yarnpkg/*"] + }, "compressionLevel": { "_package": "@yarnpkg/core", "type": ["number", "string"], diff --git a/packages/plugin-git/sources/gitUtils.ts b/packages/plugin-git/sources/gitUtils.ts index cbc90d2180b7..e05c94f6779a 100644 --- a/packages/plugin-git/sources/gitUtils.ts +++ b/packages/plugin-git/sources/gitUtils.ts @@ -123,11 +123,21 @@ export function normalizeLocator(locator: Locator) { } export function validateRepoUrl(url: string, {configuration}: {configuration: Configuration}) { - const normalizedRepoUrl = normalizeRepoUrl(url, {git: true}); + const {repo} = splitRepoUrl(url); + const normalizedRepoUrl = normalizeRepoUrl(repo, {git: true}); + const networkSettings = httpUtils.getNetworkSettings(`https://${GitUrlParse(normalizedRepoUrl).resource}`, {configuration}); if (!networkSettings.enableNetwork) throw new ReportError(MessageName.NETWORK_DISABLED, `Request to '${normalizedRepoUrl}' has been blocked because of your configuration settings`); + const approvedGitRepositoriesPattern = miscUtils.buildIgnorePattern(configuration.get(`approvedGitRepositories`)); + if (approvedGitRepositoriesPattern === null || !normalizedRepoUrl.match(approvedGitRepositoriesPattern)) { + throw new ReportError( + MessageName.NETWORK_DISABLED, + `Request to '${normalizedRepoUrl}' has been blocked because it doesn't match any of the patterns in 'approvedGitRepositories'`, + ); + } + return normalizedRepoUrl; } diff --git a/packages/plugin-git/sources/index.ts b/packages/plugin-git/sources/index.ts index 7d5afab95c23..276d2324e917 100644 --- a/packages/plugin-git/sources/index.ts +++ b/packages/plugin-git/sources/index.ts @@ -24,6 +24,7 @@ export interface Hooks { declare module '@yarnpkg/core' { interface ConfigurationValueMap { + approvedGitRepositories: Array; changesetBaseRefs: Array; changesetIgnorePatterns: Array; cloneConcurrency: number; @@ -32,6 +33,12 @@ declare module '@yarnpkg/core' { const plugin: Plugin = { configuration: { + approvedGitRepositories: { + description: `Array of git repository URL glob patterns that are allowed to be fetched`, + type: SettingsType.STRING, + default: [], + isArray: true, + }, changesetBaseRefs: { description: `The base git refs that the current HEAD is compared against when detecting changes. Supports git branches, tags, and commits.`, type: SettingsType.STRING, From 9bde4ffab5d6c18c9ab4cc44e775b8621192e553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Tue, 31 Mar 2026 12:38:01 +0200 Subject: [PATCH 2/3] Versions --- .yarn/versions/e1c54913.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .yarn/versions/e1c54913.yml diff --git a/.yarn/versions/e1c54913.yml b/.yarn/versions/e1c54913.yml new file mode 100644 index 000000000000..0abb488cd78e --- /dev/null +++ b/.yarn/versions/e1c54913.yml @@ -0,0 +1,25 @@ +releases: + "@yarnpkg/cli": minor + "@yarnpkg/plugin-git": minor + +declined: + - "@yarnpkg/plugin-compat" + - "@yarnpkg/plugin-constraints" + - "@yarnpkg/plugin-dlx" + - "@yarnpkg/plugin-essentials" + - "@yarnpkg/plugin-github" + - "@yarnpkg/plugin-init" + - "@yarnpkg/plugin-interactive-tools" + - "@yarnpkg/plugin-nm" + - "@yarnpkg/plugin-npm-cli" + - "@yarnpkg/plugin-pack" + - "@yarnpkg/plugin-patch" + - "@yarnpkg/plugin-pnp" + - "@yarnpkg/plugin-pnpm" + - "@yarnpkg/plugin-stage" + - "@yarnpkg/plugin-typescript" + - "@yarnpkg/plugin-version" + - "@yarnpkg/plugin-workspace-tools" + - "@yarnpkg/builder" + - "@yarnpkg/core" + - "@yarnpkg/doctor" From f704c50a0a35ca11a1bfa5329a6c454d40653490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Tue, 31 Mar 2026 23:37:36 +0200 Subject: [PATCH 3/3] Adds a migration and fixes tests --- .../pkg-tests-specs/sources/commands/install.test.ts | 3 ++- .../pkg-tests-specs/sources/features/checkResolutions.test.ts | 3 +++ packages/plugin-essentials/sources/commands/install.ts | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/commands/install.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/commands/install.test.ts index 7c589dfe7a27..fe0f23b64d7b 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/commands/install.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/commands/install.test.ts @@ -61,7 +61,7 @@ describe(`Commands`, () => { ); test( - `it should migrate old lockfiles by setting enableScripts to true when unset`, + `it should migrate old lockfiles by setting enableScripts and approvedGitRepositories when unset`, makeTemporaryEnv({ dependencies: { [`no-deps`]: `1.0.0`, @@ -92,6 +92,7 @@ describe(`Commands`, () => { await run(`install`); await expect(xfs.readFilePromise(rcPath, `utf8`)).resolves.toContain(`enableScripts: true`); + await expect(xfs.readFilePromise(rcPath, `utf8`)).resolves.toMatch(/approvedGitRepositories:\r?\n\s*-\s*['"]?\*\*['"]?/); }), ); diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/features/checkResolutions.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/features/checkResolutions.test.ts index 7106c5713fe3..e7176de75797 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/features/checkResolutions.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/features/checkResolutions.test.ts @@ -22,6 +22,9 @@ describe(`Features`, () => { // We don't care about this flag; in an actual attack, // the hash would be correct checksumBehavior: `ignore`, + approvedGitRepositories: [ + `https://github.com/yarnpkg/util-deprecate.git`, + ], }, async ({path, run, source}) => { await run(`add`, replacement); diff --git a/packages/plugin-essentials/sources/commands/install.ts b/packages/plugin-essentials/sources/commands/install.ts index 804ada64fa3b..0a0bca3ac818 100644 --- a/packages/plugin-essentials/sources/commands/install.ts +++ b/packages/plugin-essentials/sources/commands/install.ts @@ -23,6 +23,10 @@ const LOCKFILE_MIGRATION_RULES: Array<{ selector: v => v !== -1 && v < 8, name: `compressionLevel`, value: `mixed`, +}, { + selector: v => v < 9, + name: `approvedGitRepositories` as keyof ConfigurationValueMap, + value: [`**`], }, { selector: v => v < 9, name: `enableScripts`,