Add allowedExtensions setting (#17695)

This commit is contained in:
kevinjwang1
2026-01-30 19:43:51 +00:00
committed by GitHub
parent 12531a06f8
commit 6396ab1ccb
6 changed files with 178 additions and 7 deletions

View File

@@ -107,13 +107,14 @@ they appear in the UI.
### Security ### Security
| UI Label | Setting | Description | Default | | UI Label | Setting | Description | Default |
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | ------- | | ------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` | | Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` | | Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` | | Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` | | Extension Source Regex Allowlist | `security.allowedExtensions` | List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting. | `[]` |
| Enable Environment Variable Redaction | `security.environmentVariableRedaction.enabled` | Enable redaction of environment variables that may contain secrets. | `false` | | Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` |
| Enable Environment Variable Redaction | `security.environmentVariableRedaction.enabled` | Enable redaction of environment variables that may contain secrets. | `false` |
### Experimental ### Experimental

View File

@@ -773,6 +773,13 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false` - **Default:** `false`
- **Requires restart:** Yes - **Requires restart:** Yes
- **`security.allowedExtensions`** (array):
- **Description:** List of Regex patterns for allowed extensions. If nonempty,
only extensions that match the patterns in this list are allowed. Overrides
the blockGitExtensions setting.
- **Default:** `[]`
- **Requires restart:** Yes
- **`security.folderTrust.enabled`** (boolean): - **`security.folderTrust.enabled`** (boolean):
- **Description:** Setting to track whether Folder trust is enabled. - **Description:** Setting to track whether Folder trust is enabled.
- **Default:** `false` - **Default:** `false`

View File

@@ -144,6 +144,26 @@ export class ExtensionManager extends ExtensionLoader {
previousExtensionConfig?: ExtensionConfig, previousExtensionConfig?: ExtensionConfig,
): Promise<GeminiCLIExtension> { ): Promise<GeminiCLIExtension> {
if ( if (
this.settings.security?.allowedExtensions &&
this.settings.security?.allowedExtensions.length > 0
) {
const extensionAllowed = this.settings.security?.allowedExtensions.some(
(pattern) => {
try {
return new RegExp(pattern).test(installMetadata.source);
} catch (e) {
throw new Error(
`Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`,
);
}
},
);
if (!extensionAllowed) {
throw new Error(
`Installing extension from source "${installMetadata.source}" is not allowed by the "allowedExtensions" security setting.`,
);
}
} else if (
(installMetadata.type === 'git' || (installMetadata.type === 'git' ||
installMetadata.type === 'github-release') && installMetadata.type === 'github-release') &&
this.settings.security.blockGitExtensions this.settings.security.blockGitExtensions
@@ -152,6 +172,7 @@ export class ExtensionManager extends ExtensionLoader {
'Installing extensions from remote sources is disallowed by your current settings.', 'Installing extensions from remote sources is disallowed by your current settings.',
); );
} }
const isUpdate = !!previousExtensionConfig; const isUpdate = !!previousExtensionConfig;
let newExtensionConfig: ExtensionConfig | null = null; let newExtensionConfig: ExtensionConfig | null = null;
let localSourcePath: string | undefined; let localSourcePath: string | undefined;
@@ -522,10 +543,39 @@ Would you like to attempt to install via "git clone" instead?`,
const installMetadata = loadInstallMetadata(extensionDir); const installMetadata = loadInstallMetadata(extensionDir);
let effectiveExtensionPath = extensionDir; let effectiveExtensionPath = extensionDir;
if ( if (
this.settings.security?.allowedExtensions &&
this.settings.security?.allowedExtensions.length > 0
) {
if (!installMetadata?.source) {
throw new Error(
`Failed to load extension ${extensionDir}. The ${INSTALL_METADATA_FILENAME} file is missing or misconfigured.`,
);
}
const extensionAllowed = this.settings.security?.allowedExtensions.some(
(pattern) => {
try {
return new RegExp(pattern).test(installMetadata?.source);
} catch (e) {
throw new Error(
`Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`,
);
}
},
);
if (!extensionAllowed) {
debugLogger.warn(
`Failed to load extension ${extensionDir}. This extension is not allowed by the "allowedExtensions" security setting.`,
);
return null;
}
} else if (
(installMetadata?.type === 'git' || (installMetadata?.type === 'git' ||
installMetadata?.type === 'github-release') && installMetadata?.type === 'github-release') &&
this.settings.security.blockGitExtensions this.settings.security.blockGitExtensions
) { ) {
debugLogger.warn(
`Failed to load extension ${extensionDir}. Extensions from remote sources is disallowed by your current settings.`,
);
return null; return null;
} }

View File

@@ -622,6 +622,7 @@ describe('extension tests', () => {
}); });
it('should not load github extensions if blockGitExtensions is set', async () => { it('should not load github extensions if blockGitExtensions is set', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
createExtension({ createExtension({
extensionsDir: userExtensionsDir, extensionsDir: userExtensionsDir,
name: 'my-ext', name: 'my-ext',
@@ -645,6 +646,73 @@ describe('extension tests', () => {
const extension = extensions.find((e) => e.name === 'my-ext'); const extension = extensions.find((e) => e.name === 'my-ext');
expect(extension).toBeUndefined(); expect(extension).toBeUndefined();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'Extensions from remote sources is disallowed by your current settings.',
),
);
consoleSpy.mockRestore();
});
it('should load allowed extensions if the allowlist is set.', async () => {
createExtension({
extensionsDir: userExtensionsDir,
name: 'my-ext',
version: '1.0.0',
installMetadata: {
type: 'git',
source: 'http://allowed.com/foo/bar',
},
});
const extensionAllowlistSetting = createTestMergedSettings({
security: {
allowedExtensions: ['\\b(https?:\\/\\/)?(www\\.)?allowed\\.com\\S*'],
},
});
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: extensionAllowlistSetting,
});
const extensions = await extensionManager.loadExtensions();
expect(extensions).toHaveLength(1);
expect(extensions[0].name).toBe('my-ext');
});
it('should not load disallowed extensions if the allowlist is set.', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
createExtension({
extensionsDir: userExtensionsDir,
name: 'my-ext',
version: '1.0.0',
installMetadata: {
type: 'git',
source: 'http://notallowed.com/foo/bar',
},
});
const extensionAllowlistSetting = createTestMergedSettings({
security: {
allowedExtensions: ['\\b(https?:\\/\\/)?(www\\.)?allowed\\.com\\S*'],
},
});
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: extensionAllowlistSetting,
});
const extensions = await extensionManager.loadExtensions();
const extension = extensions.find((e) => e.name === 'my-ext');
expect(extension).toBeUndefined();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'This extension is not allowed by the "allowedExtensions" security setting',
),
);
consoleSpy.mockRestore();
}); });
it('should not load any extensions if admin.extensions.enabled is false', async () => { it('should not load any extensions if admin.extensions.enabled is false', async () => {
@@ -1116,6 +1184,30 @@ describe('extension tests', () => {
); );
}); });
it('should not install a disallowed extension if the allowlist is set', async () => {
const gitUrl = 'https://somehost.com/somerepo.git';
const allowedExtensionsSetting = createTestMergedSettings({
security: {
allowedExtensions: ['\\b(https?:\\/\\/)?(www\\.)?allowed\\.com\\S*'],
},
});
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: allowedExtensionsSetting,
});
await extensionManager.loadExtensions();
await expect(
extensionManager.installOrUpdateExtension({
source: gitUrl,
type: 'git',
}),
).rejects.toThrow(
`Installing extension from source "${gitUrl}" is not allowed by the "allowedExtensions" security setting.`,
);
});
it('should prompt for trust if workspace is not trusted', async () => { it('should prompt for trust if workspace is not trusted', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({ vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: false, isTrusted: false,

View File

@@ -1268,6 +1268,17 @@ const SETTINGS_SCHEMA = {
description: 'Blocks installing and loading extensions from Git.', description: 'Blocks installing and loading extensions from Git.',
showInDialog: true, showInDialog: true,
}, },
allowedExtensions: {
type: 'array',
label: 'Extension Source Regex Allowlist',
category: 'Security',
requiresRestart: true,
default: [] as string[],
description:
'List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting.',
showInDialog: true,
items: { type: 'string' },
},
folderTrust: { folderTrust: {
type: 'object', type: 'object',
label: 'Folder Trust', label: 'Folder Trust',

View File

@@ -1273,6 +1273,16 @@
"default": false, "default": false,
"type": "boolean" "type": "boolean"
}, },
"allowedExtensions": {
"title": "Extension Source Regex Allowlist",
"description": "List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting.",
"markdownDescription": "List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting.\n\n- Category: `Security`\n- Requires restart: `yes`\n- Default: `[]`",
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"folderTrust": { "folderTrust": {
"title": "Folder Trust", "title": "Folder Trust",
"description": "Settings for folder trust.", "description": "Settings for folder trust.",