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: 0 additions & 7 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,9 @@
"vscode": {
"extensions": [
"anthropic.claude-code",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"eamodio.gitlens"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.profiles.linux": {
"bash": {
Expand Down
43 changes: 43 additions & 0 deletions src/core/git/gitRemoteParse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,38 @@ export const isValidShorthand = (remoteValue: string): boolean => {
return validShorthandRegex.test(remoteValue);
};

/**
* Check if a URL is an Azure DevOps repository URL by validating the hostname.
* This uses proper URL parsing to avoid security issues with substring matching.
*/
const isAzureDevOpsUrl = (remoteValue: string): boolean => {
// Handle SSH URLs (e.g., git@ssh.dev.azure.com:v3/org/project/repo)
if (remoteValue.startsWith('git@ssh.dev.azure.com:')) {
return true;
}

// Handle HTTP(S) URLs
try {
const url = new URL(remoteValue);
const hostname = url.hostname.toLowerCase();

// Check for exact Azure DevOps hostnames
if (hostname === 'dev.azure.com' || hostname === 'ssh.dev.azure.com') {
return true;
}

// Check for legacy Visual Studio Team Services (*.visualstudio.com)
if (hostname.endsWith('.visualstudio.com')) {
return true;
}

return false;
} catch {
// Not a valid URL, let git-url-parse handle it
return false;
}
};

export const parseRemoteValue = (
remoteValue: string,
refs: string[] = [],
Expand All @@ -31,6 +63,17 @@ export const parseRemoteValue = (
};
}

// Check for Azure DevOps URLs before parsing, as git-url-parse may not handle them correctly
// - SSH: git@ssh.dev.azure.com:v3/org/project/repo
// - HTTPS: https://dev.azure.com/organization/project/_git/repo
// - Legacy: https://org.visualstudio.com/project/_git/repo
if (isAzureDevOpsUrl(remoteValue)) {
return {
repoUrl: remoteValue,
remoteBranch: undefined,
};
}

try {
const parsedFields = gitUrlParse(remoteValue, refs) as IGitUrl;

Expand Down
42 changes: 42 additions & 0 deletions tests/core/git/gitRemoteParse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,48 @@ describe('remoteAction functions', () => {
});
});

test('should handle Azure DevOps SSH URLs', () => {
const azureDevOpsUrl = 'git@ssh.dev.azure.com:v3/organization/project/repo';
const parsed = parseRemoteValue(azureDevOpsUrl);
expect(parsed).toEqual({
repoUrl: azureDevOpsUrl,
remoteBranch: undefined,
});
});

test('should handle Azure DevOps HTTPS URLs', () => {
const azureDevOpsUrl = 'https://dev.azure.com/organization/project/_git/repo';
const parsed = parseRemoteValue(azureDevOpsUrl);
expect(parsed).toEqual({
repoUrl: azureDevOpsUrl,
remoteBranch: undefined,
});
});
Comment thread
yamadashy marked this conversation as resolved.

test('should handle legacy Visual Studio Team Services URLs', () => {
const vstsUrl = 'https://myorg.visualstudio.com/myproject/_git/myrepo';
const parsed = parseRemoteValue(vstsUrl);
expect(parsed).toEqual({
repoUrl: vstsUrl,
remoteBranch: undefined,
});
});

test('should not treat URLs with Azure DevOps hostnames in path as Azure DevOps URLs', () => {
// Security test: Ensure URLs with Azure DevOps keywords in the path are not treated as Azure DevOps
const maliciousUrl = 'https://evil.com/dev.azure.com/fake/repo';
const parsed = parseRemoteValue(maliciousUrl);
// Should be parsed normally (not as Azure DevOps), with .git suffix added
expect(parsed.repoUrl).toBe('https://evil.com/dev.azure.com/fake/repo.git');
});

test('should not treat URLs with visualstudio.com in path as Azure DevOps URLs', () => {
const maliciousUrl = 'https://evil.com/path/visualstudio.com/fake/repo';
const parsed = parseRemoteValue(maliciousUrl);
// Should be parsed normally (not as Azure DevOps), with .git suffix added
expect(parsed.repoUrl).toBe('https://evil.com/path/visualstudio.com/fake/repo.git');
});

test('should get correct branch name from url', () => {
expect(parseRemoteValue('https://github.com/username/repo/tree/branchname')).toEqual({
repoUrl: 'https://github.com/username/repo.git',
Expand Down
Loading