Skip to content

chore(eslint): Add eslint rule to flag new RegExp() usage #10009

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 2, 2024
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
1 change: 1 addition & 0 deletions packages/astro/src/server/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function isValidBaggageString(baggage?: string): boolean {
const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+";
const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+';
const spaces = '\\s*';
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp for readability, no user input
const baggageRegex = new RegExp(
`^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`,
);
Expand Down
5 changes: 5 additions & 0 deletions packages/eslint-config-sdk/src/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ module.exports = {
'@sentry-internal/sdk/no-optional-chaining': 'error',
'@sentry-internal/sdk/no-nullish-coalescing': 'error',

// We want to avoid using the RegExp constructor as it can lead to invalid or dangerous regular expressions
// if end user input is used in the constructor. It's fine to ignore this rule if it is safe to use the RegExp.
// However, we want to flag each use case so that we're aware of the potential danger.
'@sentry-internal/sdk/no-regexp-constructor': 'error',

// JSDOC comments are required for classes and methods. As we have a public facing codebase, documentation,
// even if it may seems excessive at times, is important to emphasize. Turned off in tests.
'jsdoc/require-jsdoc': [
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin-sdk/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ module.exports = {
'no-eq-empty': require('./rules/no-eq-empty'),
'no-unsupported-es6-methods': require('./rules/no-unsupported-es6-methods'),
'no-class-field-initializers': require('./rules/no-class-field-initializers'),
'no-regexp-constructor': require('./rules/no-regexp-constructor'),
},
};
29 changes: 29 additions & 0 deletions packages/eslint-plugin-sdk/src/rules/no-regexp-constructor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

/**
* This rule was created to flag usages of the RegExp constructor.
* It is fine to use `new RegExp` if it is necessary and safe to do so.
* "safe" means, that the regular expression is NOT created from unchecked or unescaped user input.
* Avoid creating regular expressions from user input, because it can lead to invalid expressions or vulnerabilities.
*/
module.exports = {
meta: {
docs: {
description:
"Avoid using `new RegExp` constructor to ensure we don't accidentally create invalid or dangerous regular expressions from user input.",
},
schema: [],
},
create: function (context) {
return {
NewExpression(node) {
if (node.callee.type === 'Identifier' && node.callee.name === 'RegExp') {
context.report({
node,
message: 'Avoid using the RegExp() constructor with unsafe user input.',
});
}
},
};
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ function convertNextRouteToRegExp(route: string): RegExp {
)
.join('/');

// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- routeParts are from the build manifest, so no raw user input
return new RegExp(
`^${rejoinedRouteParts}${optionalCatchallWildcardRegex}(?:/)?$`, // optional slash at the end
);
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/config/loaders/prefixLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default function prefixLoader(this: LoaderThis<LoaderOptions>, userCode:
// Fill in placeholders
let templateCode = fs.readFileSync(templatePath).toString();
replacements.forEach(([placeholder, value]) => {
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- user input is escaped
const placeholderRegex = new RegExp(escapeStringForRegex(placeholder), 'g');
templateCode = templateCode.replace(placeholderRegex, value);
});
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/config/loaders/wrappingLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export default function wrappingLoader(
// Add a slash at the beginning
.replace(/(.*)/, '/$1')
// Pull off the file extension
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input
.replace(new RegExp(`\\.(${pageExtensionRegex})`), '')
// Any page file named `index` corresponds to root of the directory its in, URL-wise, so turn `/xyz/index` into
// just `/xyz`
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ function isMatchingRule(rule: WebpackModuleRule, projectDir: string): boolean {
//
// The next 11 option is ugly, but thankfully 'next', 'babel', and 'loader' do appear in it in the same order as in
// 'next-babel-loader', so we can use the same regex to test for both.
if (!useEntries.some(entry => entry?.loader && new RegExp('next.*(babel|swc).*loader').test(entry.loader))) {
if (!useEntries.some(entry => entry?.loader && /next.*(babel|swc).*loader/.test(entry.loader))) {
return false;
}

Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/edge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function init(options: VercelEdgeOptions = {}): void {

// Normally we would use `path.resolve` to obtain the absolute path we will strip from the stack frame to align with
// the uploaded artifacts, however we don't have access to that API in edge so we need to be a bit more lax.
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- user input is escaped
const SOURCEMAP_FILENAME_REGEX = new RegExp(`.*${escapeStringForRegex(distDirAbsPath)}`);

const defaultRewriteFramesIntegration = new RewriteFrames({
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ function addServerIntegrations(options: NodeOptions): void {
// nextjs always puts the build directory at the project root level, which is also where you run `next start` from, so
// we can read in the project directory from the currently running process
const distDirAbsPath = path.resolve(distDirName).replace(/(\/|\\)$/, ''); // We strip trailing slashes because "app:///_next" also doesn't have one
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- user input is escaped
const SOURCEMAP_FILENAME_REGEX = new RegExp(escapeStringForRegex(distDirAbsPath));

const defaultRewriteFramesIntegration = new RewriteFrames({
Expand Down
1 change: 1 addition & 0 deletions packages/sveltekit/src/server/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function rewriteFramesIteratee(frame: StackFrame): StackFrame {
let strippedFilename;
if (svelteKitBuildOutDir) {
strippedFilename = filename.replace(
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input + escaped anyway
new RegExp(`^.*${escapeStringForRegex(join(svelteKitBuildOutDir, 'server'))}/`),
'',
);
Expand Down
2 changes: 2 additions & 0 deletions packages/sveltekit/src/vite/sourceMaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi

transform: async (code, id) => {
let modifiedCode = code;
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input + escaped anyway
const isServerHooksFile = new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`).test(id);

if (isServerHooksFile) {
Expand Down Expand Up @@ -195,6 +196,7 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi
if (fs.existsSync(mapFile)) {
const mapContent = (await fs.promises.readFile(mapFile, 'utf-8')).toString();
const cleanedMapContent = mapContent.replace(
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- no user input + escaped anyway
new RegExp(escapeStringForRegex(WRAPPED_MODULE_SUFFIX), 'gm'),
'',
);
Expand Down
1 change: 1 addition & 0 deletions packages/tracing-internal/src/node/integrations/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ export const extractOriginalRoute = (
const orderedKeys = keys.sort((a, b) => a.offset - b.offset);

// add d flag for getting indices from regexp result
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- regexp comes from express.js
const pathRegex = new RegExp(regexp, `${regexp.flags}d`);
/**
* use custom type cause of TS error with missing indices in RegExpExecArray
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { DynamicSamplingContext, PropagationContext, TraceparentData } from
import { baggageHeaderToDynamicSamplingContext } from './baggage';
import { uuid4 } from './misc';

// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp is used for readability here
export const TRACEPARENT_REGEXP = new RegExp(
'^[ \\t]*' + // whitespace
'([0-9a-f]{32})?' + // trace_id
Expand Down