Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- Added `toggle_connect_hardware_keyboard` tool to toggle the iOS Simulator hardware keyboard connection ([#346](https://github.com/getsentry/XcodeBuildMCP/issues/346)).
- Fixed `xcode_tools_bridge_disconnect` immediately re-syncing proxied tools after a manual disconnect ([#343](https://github.com/getsentry/XcodeBuildMCP/issues/343)).
- Stopped suggesting an unsupported `--device-id`/`deviceId` argument in the `device list` next-step hint for `device build`/`build_device`; device targeting flows through session defaults ([#350](https://github.com/getsentry/XcodeBuildMCP/pull/350) by [@MukundaKatta](https://github.com/MukundaKatta)).
- Extracted shared version validation (`VERSION_REGEX`/`validateVersion`) for version generation and tests, and switched generated `src/version.ts` literals back to single quotes to avoid Prettier conflicts ([#368](https://github.com/getsentry/XcodeBuildMCP/issues/368), [#369](https://github.com/getsentry/XcodeBuildMCP/issues/369)).

## [2.3.2]

Expand Down
1 change: 0 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export default [
'coverage/**',
'src/core/generated-plugins.ts',
'src/core/generated-resources.ts',
'src/version.ts',
],
},
{
Expand Down
23 changes: 7 additions & 16 deletions scripts/generate-version.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { validateVersion } from '../src/utils/version/version-validation.ts';

interface PackageJson {
name: string;
Expand All @@ -19,16 +20,6 @@ function parseGitHubOwnerAndName(url: string): { owner: string; name: string } {
return { owner: match[1], name: match[2] };
}

const VERSION_REGEX = /^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.\-]+)?(\+[a-zA-Z0-9.\-]+)?$/;

function validateVersion(name: string, value: string): void {
if (!VERSION_REGEX.test(value)) {
throw new Error(
`Invalid ${name} in package.json: ${JSON.stringify(value)}. Expected a version string.`,
);
}
}

async function main(): Promise<void> {
const repoRoot = process.cwd();
const packagePath = path.join(repoRoot, 'package.json');
Expand All @@ -49,12 +40,12 @@ async function main(): Promise<void> {
validateVersion('macOSTemplateVersion', pkg.macOSTemplateVersion);

const content =
`export const version = ${JSON.stringify(pkg.version)};\n` +
`export const iOSTemplateVersion = ${JSON.stringify(pkg.iOSTemplateVersion)};\n` +
`export const macOSTemplateVersion = ${JSON.stringify(pkg.macOSTemplateVersion)};\n` +
`export const packageName = ${JSON.stringify(pkg.name)};\n` +
`export const repositoryOwner = ${JSON.stringify(repo.owner)};\n` +
`export const repositoryName = ${JSON.stringify(repo.name)};\n`;
`export const version = '${pkg.version}';\n` +
`export const iOSTemplateVersion = '${pkg.iOSTemplateVersion}';\n` +
`export const macOSTemplateVersion = '${pkg.macOSTemplateVersion}';\n` +
`export const packageName = '${pkg.name}';\n` +
`export const repositoryOwner = '${repo.owner}';\n` +
`export const repositoryName = '${repo.name}';\n`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unescaped interpolation of unvalidated values into generated code

Medium Severity

The switch from JSON.stringify(...) to single-quote template literal interpolation removes escaping for pkg.name, repo.owner, and repo.name — none of which are validated by validateVersion(). If any of these values contain a single quote, backslash, or other special character, the generated src/version.ts would contain broken or injectable TypeScript. The prior JSON.stringify defense-in-depth was deliberate and its corresponding tests have also been removed.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit beb6e1b. Configure here.


await writeFile(versionPath, content, 'utf8');
}
Expand Down
31 changes: 8 additions & 23 deletions src/utils/__tests__/generate-version-validation.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { describe, it, expect } from 'vitest';

// We cannot easily import the generate-version script (it runs main() immediately),
// so we extract and test the core logic: VERSION_REGEX and JSON.stringify defense.

const VERSION_REGEX = /^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.\-]+)?(\+[a-zA-Z0-9.\-]+)?$/;
import { VERSION_REGEX, validateVersion } from '../version/version-validation.ts';

describe('generate-version: VERSION_REGEX validation', () => {
it('accepts standard semver', () => {
Expand Down Expand Up @@ -47,25 +43,14 @@ describe('generate-version: VERSION_REGEX validation', () => {
});
});

describe('generate-version: JSON.stringify defense-in-depth', () => {
it('produces safe code even if a value somehow contains quotes', () => {
const malicious = "1.0.0'; process.exit(1); //";
const generated = `const version = ${JSON.stringify(malicious)};\n`;
// The output should use escaped double-quoted string, not break out
expect(generated).toContain('"1.0.0');
expect(generated).not.toContain("'1.0.0'; process.exit(1)");
// Should be parseable JS (using const instead of export for Function() compat)
expect(() => new Function(generated)).not.toThrow();
describe('generate-version: validateVersion', () => {
it('throws for invalid versions', () => {
expect(() => validateVersion('version', "1.0.0'; process.exit(1); //")).toThrow(
/Invalid version in package\.json/,
);
});

it('JSON.stringify properly escapes backslashes and control characters', () => {
const tricky = '1.0.0\n";process.exit(1);//';
const serialized = JSON.stringify(tricky);
// The newline should be escaped as \\n, and the quote should be escaped
expect(serialized).toContain('\\n');
expect(serialized).toContain('\\"');
// The resulting assignment should be valid JS
const code = `const v = ${serialized};`;
expect(() => new Function(code)).not.toThrow();
it('does not throw for valid versions', () => {
expect(() => validateVersion('version', '1.0.0-alpha.1+meta')).not.toThrow();
});
});
9 changes: 9 additions & 0 deletions src/utils/version/version-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const VERSION_REGEX = /^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.\-]+)?(\+[a-zA-Z0-9.\-]+)?$/;

export function validateVersion(name: string, value: string): void {
if (!VERSION_REGEX.test(value)) {
throw new Error(
`Invalid ${name} in package.json: ${JSON.stringify(value)}. Expected a version string.`,
);
}
}
Loading