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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,7 @@
"@mongodb-js/compass-components": "^1.68.0",
"@mongodb-js/connection-form": "^1.85.0",
"@mongodb-js/connection-info": "^0.26.3",
"@mongodb-js/connection-string-policy": "^0.2.4",
"@mongodb-js/device-id": "^0.4.16",
"@mongodb-js/mongodb-constants": "^0.16.1",
"@mongodb-js/mongodb-schema": "^12.7.10",
Expand Down Expand Up @@ -1302,7 +1303,6 @@
"mongodb-query-parser": "^5.0.3",
"node-machine-id": "1.1.12",
"numeral": "^2.0.6",
"query-string": "^7.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "^9.3.0",
Expand Down
64 changes: 26 additions & 38 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 62 additions & 5 deletions src/mdbExtensionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ import {
DocumentEditedTelemetryEvent,
} from './telemetry';

import * as queryString from 'query-string';
import { z } from 'zod';
import { MCPController } from './mcp/mcpController';
import formatError from './utils/formatError';
Expand Down Expand Up @@ -164,6 +163,67 @@ const DEEP_LINK_PARAM_SCHEMA: Partial<
}),
};

// Every parameter name the commands above accept, plus the tracking parameter that
// `_handleDeepLink` consumes itself.
const DEEP_LINK_PARAM_NAMES = [
'utm_source',
...Object.values(DEEP_LINK_PARAM_SCHEMA).flatMap((schema) =>
schema ? Object.keys(schema.shape) : [],
),
];

/**
* Parse the query of a deep link into its parameters.
*
* VS Code percent-decodes `Uri.query` before the handler runs, so by the time we see it
* a connection string's own `&` and `=` are indistinguishable from the separators
* between parameters, and a `+` can no longer be told apart from an encoded space. That
* rules out parsing this as form encoding: it would read `mongodb+srv://` as
* `mongodb srv://` and split the connection string's own options out into parameters of
* their own.
*
* So we split only where a parameter name we know about begins a new parameter, and take
* the rest of each parameter verbatim. A connection string containing `&<known name>=`
* would still be split too early, but none of these names is a connection string option.
*/
function parseDeepLinkQuery(query: string): Record<string, unknown> {
// A deep link is untrusted input, so the parameters must not inherit from
// `Object.prototype` and let a parameter named e.g. `__proto__` reach it.
const parameters: Record<string, unknown> = Object.create(null);

if (!query) {
return parameters;
}

const nextParameter = new RegExp(
`&(?=(?:${DEEP_LINK_PARAM_NAMES.join('|')})=)`,
);

for (const parameter of query.split(nextParameter)) {
const separator = parameter.indexOf('=');
const name = separator === -1 ? parameter : parameter.slice(0, separator);
const rawValue = separator === -1 ? null : parameter.slice(separator + 1);

let value: unknown = rawValue;
if (rawValue === 'true' || rawValue === 'false') {
value = rawValue === 'true';
}

// Keep repeated parameters as a list, so that they are rejected by the schema
// rather than one of them quietly winning.
if (name in parameters) {
const existing = parameters[name];
parameters[name] = Array.isArray(existing)
? [...existing, value]
: [existing, value];
} else {
parameters[name] = value;
}
}

return parameters;
}

function validateDeepLinkParams(
command: ExtensionCommand,
params: Record<string, unknown>,
Expand Down Expand Up @@ -330,10 +390,7 @@ export default class MDBExtensionController implements vscode.Disposable {
command = `mdb.${command}`;
}

const parameters = queryString.parse(uri.query, {
parseBooleans: true,
parseNumbers: true,
});
const parameters = parseDeepLinkQuery(uri.query);

const source =
'utm_source' in parameters && typeof parameters.utm_source === 'string'
Expand Down
Loading
Loading