Skip to content

Commit 4bd2b21

Browse files
authored
Adds approvedGitRepositories (#7091)
## What's the problem this PR addresses? The `enableScripts` flag doesn't affect `git:` dependencies, which always runs the `pack` script for whatever package manager the project is using. ## How did you fix it? To avoid running arbitrary code through the `git:` protocol we're introducing a new setting called `approvedGitRepositories`. This list of glob will validate the repository urls we clone. ## Checklist <!--- Don't worry if you miss something, chores are automatically tested. --> <!--- This checklist exists to help you remember doing the chores when you submit a PR. --> <!--- Put an `x` in all the boxes that apply. --> - [x] I have read the [Contributing Guide](https://yarnpkg.com/advanced/contributing). <!-- See https://yarnpkg.com/advanced/contributing#preparing-your-pr-to-be-released for more details. --> <!-- Check with `yarn version check` and fix with `yarn version check -i` --> - [x] I have set the packages that need to be released for my changes to be effective. <!-- The "Testing chores" workflow validates that your PR follows our guidelines. --> <!-- If it doesn't pass, click on it to see details as to what your PR might be missing. --> - [x] I will check that all automated PR checks pass before the PR gets reviewed.
1 parent b438c5d commit 4bd2b21

9 files changed

Lines changed: 113 additions & 2 deletions

File tree

.yarn/versions/e1c54913.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
releases:
2+
"@yarnpkg/cli": minor
3+
"@yarnpkg/plugin-git": minor
4+
5+
declined:
6+
- "@yarnpkg/plugin-compat"
7+
- "@yarnpkg/plugin-constraints"
8+
- "@yarnpkg/plugin-dlx"
9+
- "@yarnpkg/plugin-essentials"
10+
- "@yarnpkg/plugin-github"
11+
- "@yarnpkg/plugin-init"
12+
- "@yarnpkg/plugin-interactive-tools"
13+
- "@yarnpkg/plugin-nm"
14+
- "@yarnpkg/plugin-npm-cli"
15+
- "@yarnpkg/plugin-pack"
16+
- "@yarnpkg/plugin-patch"
17+
- "@yarnpkg/plugin-pnp"
18+
- "@yarnpkg/plugin-pnpm"
19+
- "@yarnpkg/plugin-stage"
20+
- "@yarnpkg/plugin-typescript"
21+
- "@yarnpkg/plugin-version"
22+
- "@yarnpkg/plugin-workspace-tools"
23+
- "@yarnpkg/builder"
24+
- "@yarnpkg/core"
25+
- "@yarnpkg/doctor"

packages/acceptance-tests/pkg-tests-specs/sources/commands/install.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe(`Commands`, () => {
6161
);
6262

6363
test(
64-
`it should migrate old lockfiles by setting enableScripts to true when unset`,
64+
`it should migrate old lockfiles by setting enableScripts and approvedGitRepositories when unset`,
6565
makeTemporaryEnv({
6666
dependencies: {
6767
[`no-deps`]: `1.0.0`,
@@ -92,6 +92,7 @@ describe(`Commands`, () => {
9292
await run(`install`);
9393

9494
await expect(xfs.readFilePromise(rcPath, `utf8`)).resolves.toContain(`enableScripts: true`);
95+
await expect(xfs.readFilePromise(rcPath, `utf8`)).resolves.toMatch(/approvedGitRepositories:\r?\n\s*-\s*['"]?\*\*['"]?/);
9596
}),
9697
);
9798

packages/acceptance-tests/pkg-tests-specs/sources/features/checkResolutions.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ describe(`Features`, () => {
2222
// We don't care about this flag; in an actual attack,
2323
// the hash would be correct
2424
checksumBehavior: `ignore`,
25+
approvedGitRepositories: [
26+
`https://github.com/yarnpkg/util-deprecate.git`,
27+
],
2528
}, async ({path, run, source}) => {
2629
await run(`add`, replacement);
2730

packages/acceptance-tests/pkg-tests-specs/sources/protocols/git.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ const TESTED_URLS = {
2121
[`https://github.com/yarnpkg/util-deprecate.git#b3562c2798507869edb767da869cd7b85487726d`]: {version: `1.0.0`, runOnCI: true},
2222
};
2323

24+
const defaultGitConfiguration = {
25+
approvedGitRepositories: [
26+
`http://localhost:*/repositories/*.git`,
27+
`https://github.com/yarnpkg/util-deprecate.git`,
28+
`ssh://[email protected]/yarnpkg/util-deprecate.git`,
29+
],
30+
};
31+
2432
describe(`Protocols`, () => {
2533
describe(`git:`, () => {
2634
for (const [url, {version, runOnCI}] of Object.entries(TESTED_URLS)) {
@@ -34,6 +42,7 @@ describe(`Protocols`, () => {
3442
{
3543
dependencies: {[`util-deprecate`]: url},
3644
},
45+
defaultGitConfiguration,
3746
async ({path, run, source}) => {
3847
await run(`install`);
3948

@@ -54,6 +63,7 @@ describe(`Protocols`, () => {
5463
[`has-prepack`]: tests.startPackageServer().then(url => `${url}/repositories/has-prepack.git`),
5564
},
5665
},
66+
defaultGitConfiguration,
5767
async ({path, run, source}) => {
5868
await run(`install`);
5969

@@ -70,6 +80,7 @@ describe(`Protocols`, () => {
7080
[`no-prepack`]: tests.startPackageServer().then(url => `${url}/repositories/no-prepack.git`),
7181
},
7282
},
83+
defaultGitConfiguration,
7384
async ({path, run, source}) => {
7485
await run(`install`);
7586

@@ -87,6 +98,7 @@ describe(`Protocols`, () => {
8798
[`pkg-b`]: tests.startPackageServer().then(url => `${url}/repositories/deep-projects.git#cwd=projects/pkg-b`),
8899
},
89100
},
101+
defaultGitConfiguration,
90102
async ({path, run, source}) => {
91103
await run(`install`);
92104

@@ -103,6 +115,25 @@ describe(`Protocols`, () => {
103115
),
104116
);
105117

118+
test(
119+
`it should block git dependencies from repositories that aren't approved`,
120+
makeTemporaryEnv(
121+
{
122+
dependencies: {
123+
[`has-prepack`]: tests.startPackageServer().then(url => `${url}/repositories/has-prepack.git`),
124+
},
125+
},
126+
{
127+
approvedGitRepositories: [`https://github.com/yarnpkg/*`],
128+
},
129+
async ({run}) => {
130+
await expect(run(`install`)).rejects.toThrow(
131+
/doesn't match any of the patterns in 'approvedGitRepositories'/,
132+
);
133+
},
134+
),
135+
);
136+
106137
test(
107138
`it should support installing workspace packages from projects in subfolders`,
108139
makeTemporaryEnv(
@@ -112,6 +143,7 @@ describe(`Protocols`, () => {
112143
[`lib-b`]: tests.startPackageServer().then(url => `${url}/repositories/deep-projects.git#cwd=projects/pkg-b&workspace=lib`),
113144
},
114145
},
146+
defaultGitConfiguration,
115147
async ({path, run, source}) => {
116148
await run(`install`);
117149

@@ -140,6 +172,7 @@ describe(`Protocols`, () => {
140172
[`pkg-b`]: tests.startPackageServer().then(url => `${url}/repositories/workspaces.git#workspace=pkg-b`),
141173
},
142174
},
175+
defaultGitConfiguration,
143176
async ({path, run, source}) => {
144177
await run(`install`);
145178

@@ -164,6 +197,7 @@ describe(`Protocols`, () => {
164197
[`yarn-1-project`]: tests.startPackageServer().then(url => `${url}/repositories/yarn-1-project.git`),
165198
},
166199
},
200+
defaultGitConfiguration,
167201
async ({path, run, source}) => {
168202
await expect(run(`install`, {
169203
env: {
@@ -188,6 +222,7 @@ describe(`Protocols`, () => {
188222
[`npm-project`]: tests.startPackageServer().then(url => `${url}/repositories/npm-project.git`),
189223
},
190224
},
225+
defaultGitConfiguration,
191226
async ({path, run, source}) => {
192227
await run(`install`);
193228

@@ -204,6 +239,7 @@ describe(`Protocols`, () => {
204239
[`npm-has-prepack`]: tests.startPackageServer().then(url => `${url}/repositories/npm-has-prepack.git`),
205240
},
206241
},
242+
defaultGitConfiguration,
207243
async ({path, run, source}) => {
208244
await expect(run(`install`, {
209245
env: {
@@ -236,6 +272,7 @@ describe(`Protocols`, () => {
236272
[`pkg-b`]: tests.startPackageServer().then(url => `${url}/repositories/npm-workspaces.git#workspace=pkg-b`),
237273
},
238274
},
275+
defaultGitConfiguration,
239276
async ({path, run, source}) => {
240277
const {code, stdout, stderr} = await execUtils.execvp(`npm`, [`--version`], {cwd: path});
241278
if (code !== 0)
@@ -271,6 +308,7 @@ describe(`Protocols`, () => {
271308
[`yarn-1-project`]: tests.startPackageServer().then(url => `${url}/repositories/yarn-1-project.git`),
272309
},
273310
},
311+
defaultGitConfiguration,
274312
async ({path, run, source}) => {
275313
// This checks that the `set version classic` part of `scriptUtils.prepareExternalProject` doesn't use Corepack.
276314
// The rest of the install will fail though.
@@ -295,6 +333,7 @@ describe(`Protocols`, () => {
295333
[`no-lockfile-project`]: tests.startPackageServer().then(url => `${url}/repositories/no-lockfile-project.git`),
296334
},
297335
},
336+
defaultGitConfiguration,
298337
async ({path, run, source}) => {
299338
await expect(run(`install`, {
300339
env: {
@@ -314,6 +353,7 @@ describe(`Protocols`, () => {
314353
[`yarn-1-project`]: tests.startPackageServer().then(url => `${url}/repositories/yarn-1-project.git`),
315354
},
316355
},
356+
defaultGitConfiguration,
317357
async ({path, run, source}) => {
318358
await expect(run(`install`)).resolves.toBeTruthy();
319359

packages/docusaurus/docs/advanced/01-general-reference/protocols/git.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ The `git:` protocol fetches packages directly from a git repository. This is use
1111
yarn add typanion@[email protected]/arcanis/typanion.git
1212
```
1313

14+
## Repository approval
15+
16+
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.
17+
18+
```yaml
19+
approvedGitRepositories:
20+
- https://github.com/yarnpkg/*
21+
- ssh://[email protected]/yarnpkg/*
22+
```
23+
1424
## Packing
1525
1626
The target repository won't be used as-is - it will first be packed using [`pack`](/cli/pack).

packages/docusaurus/static/configuration/yarnrc.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@
7171
"type": "number",
7272
"default": 2
7373
},
74+
"approvedGitRepositories": {
75+
"_package": "@yarnpkg/plugin-git",
76+
"title": "Array of git repository URL glob patterns that are allowed to be fetched.",
77+
"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.",
78+
"type": "array",
79+
"items": {
80+
"type": "string"
81+
},
82+
"default": [],
83+
"_exampleItems": ["https://github.com/yarnpkg/*", "ssh://[email protected]/yarnpkg/*"]
84+
},
7485
"compressionLevel": {
7586
"_package": "@yarnpkg/core",
7687
"type": ["number", "string"],

packages/plugin-essentials/sources/commands/install.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ const LOCKFILE_MIGRATION_RULES: Array<{
2323
selector: v => v !== -1 && v < 8,
2424
name: `compressionLevel`,
2525
value: `mixed`,
26+
}, {
27+
selector: v => v < 9,
28+
name: `approvedGitRepositories` as keyof ConfigurationValueMap,
29+
value: [`**`],
2630
}, {
2731
selector: v => v < 9,
2832
name: `enableScripts`,

packages/plugin-git/sources/gitUtils.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,21 @@ export function normalizeLocator(locator: Locator) {
123123
}
124124

125125
export function validateRepoUrl(url: string, {configuration}: {configuration: Configuration}) {
126-
const normalizedRepoUrl = normalizeRepoUrl(url, {git: true});
126+
const {repo} = splitRepoUrl(url);
127+
const normalizedRepoUrl = normalizeRepoUrl(repo, {git: true});
128+
127129
const networkSettings = httpUtils.getNetworkSettings(`https://${GitUrlParse(normalizedRepoUrl).resource}`, {configuration});
128130
if (!networkSettings.enableNetwork)
129131
throw new ReportError(MessageName.NETWORK_DISABLED, `Request to '${normalizedRepoUrl}' has been blocked because of your configuration settings`);
130132

133+
const approvedGitRepositoriesPattern = miscUtils.buildIgnorePattern(configuration.get(`approvedGitRepositories`));
134+
if (approvedGitRepositoriesPattern === null || !normalizedRepoUrl.match(approvedGitRepositoriesPattern)) {
135+
throw new ReportError(
136+
MessageName.NETWORK_DISABLED,
137+
`Request to '${normalizedRepoUrl}' has been blocked because it doesn't match any of the patterns in 'approvedGitRepositories'`,
138+
);
139+
}
140+
131141
return normalizedRepoUrl;
132142
}
133143

packages/plugin-git/sources/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface Hooks {
2424

2525
declare module '@yarnpkg/core' {
2626
interface ConfigurationValueMap {
27+
approvedGitRepositories: Array<string>;
2728
changesetBaseRefs: Array<string>;
2829
changesetIgnorePatterns: Array<string>;
2930
cloneConcurrency: number;
@@ -32,6 +33,12 @@ declare module '@yarnpkg/core' {
3233

3334
const plugin: Plugin = {
3435
configuration: {
36+
approvedGitRepositories: {
37+
description: `Array of git repository URL glob patterns that are allowed to be fetched`,
38+
type: SettingsType.STRING,
39+
default: [],
40+
isArray: true,
41+
},
3542
changesetBaseRefs: {
3643
description: `The base git refs that the current HEAD is compared against when detecting changes. Supports git branches, tags, and commits.`,
3744
type: SettingsType.STRING,

0 commit comments

Comments
 (0)