Skip to content

Commit a6589aa

Browse files
Lms24anonrig
authored andcommitted
chore(eslint): Add eslint rule to flag new RegExp() usage (#10009)
Add a new eslint rule that flags the usage of `new RegExp()` constructor calls. The purpose of this rule is to make us aware of the potential danger of creating a regular expression from (end) user input. This has led to security incidents in the past. To be clear, it is perfectly okay to ignore this rule in cases where we're sure that there's no danger or where input is already escaped.
1 parent a825174 commit a6589aa

File tree

14 files changed

+47
-1
lines changed

14 files changed

+47
-1
lines changed

packages/astro/src/server/meta.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export function isValidBaggageString(baggage?: string): boolean {
7373
const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+";
7474
const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+';
7575
const spaces = '\\s*';
76+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp for readability, no user input
7677
const baggageRegex = new RegExp(
7778
`^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`,
7879
);

packages/eslint-config-sdk/src/base.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ module.exports = {
135135
'@sentry-internal/sdk/no-optional-chaining': 'error',
136136
'@sentry-internal/sdk/no-nullish-coalescing': 'error',
137137

138+
// We want to avoid using the RegExp constructor as it can lead to invalid or dangerous regular expressions
139+
// if end user input is used in the constructor. It's fine to ignore this rule if it is safe to use the RegExp.
140+
// However, we want to flag each use case so that we're aware of the potential danger.
141+
'@sentry-internal/sdk/no-regexp-constructor': 'error',
142+
138143
// JSDOC comments are required for classes and methods. As we have a public facing codebase, documentation,
139144
// even if it may seems excessive at times, is important to emphasize. Turned off in tests.
140145
'jsdoc/require-jsdoc': [

packages/eslint-plugin-sdk/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ module.exports = {
1515
'no-eq-empty': require('./rules/no-eq-empty'),
1616
'no-unsupported-es6-methods': require('./rules/no-unsupported-es6-methods'),
1717
'no-class-field-initializers': require('./rules/no-class-field-initializers'),
18+
'no-regexp-constructor': require('./rules/no-regexp-constructor'),
1819
},
1920
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
3+
/**
4+
* This rule was created to flag usages of the RegExp constructor.
5+
* It is fine to use `new RegExp` if it is necessary and safe to do so.
6+
* "safe" means, that the regular expression is NOT created from unchecked or unescaped user input.
7+
* Avoid creating regular expressions from user input, because it can lead to invalid expressions or vulnerabilities.
8+
*/
9+
module.exports = {
10+
meta: {
11+
docs: {
12+
description:
13+
"Avoid using `new RegExp` constructor to ensure we don't accidentally create invalid or dangerous regular expressions from user input.",
14+
},
15+
schema: [],
16+
},
17+
create: function (context) {
18+
return {
19+
NewExpression(node) {
20+
if (node.callee.type === 'Identifier' && node.callee.name === 'RegExp') {
21+
context.report({
22+
node,
23+
message: 'Avoid using the RegExp() constructor with unsafe user input.',
24+
});
25+
}
26+
},
27+
};
28+
},
29+
};

packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ function convertNextRouteToRegExp(route: string): RegExp {
252252
)
253253
.join('/');
254254

255+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- routeParts are from the build manifest, so no raw user input
255256
return new RegExp(
256257
`^${rejoinedRouteParts}${optionalCatchallWildcardRegex}(?:/)?$`, // optional slash at the end
257258
);

packages/nextjs/src/config/loaders/prefixLoader.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default function prefixLoader(this: LoaderThis<LoaderOptions>, userCode:
3030
// Fill in placeholders
3131
let templateCode = fs.readFileSync(templatePath).toString();
3232
replacements.forEach(([placeholder, value]) => {
33+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- user input is escaped
3334
const placeholderRegex = new RegExp(escapeStringForRegex(placeholder), 'g');
3435
templateCode = templateCode.replace(placeholderRegex, value);
3536
});

packages/nextjs/src/config/loaders/wrappingLoader.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export default function wrappingLoader(
115115
// Add a slash at the beginning
116116
.replace(/(.*)/, '/$1')
117117
// Pull off the file extension
118+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input
118119
.replace(new RegExp(`\\.(${pageExtensionRegex})`), '')
119120
// Any page file named `index` corresponds to root of the directory its in, URL-wise, so turn `/xyz/index` into
120121
// just `/xyz`

packages/nextjs/src/config/webpack.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ function isMatchingRule(rule: WebpackModuleRule, projectDir: string): boolean {
462462
//
463463
// The next 11 option is ugly, but thankfully 'next', 'babel', and 'loader' do appear in it in the same order as in
464464
// 'next-babel-loader', so we can use the same regex to test for both.
465-
if (!useEntries.some(entry => entry?.loader && new RegExp('next.*(babel|swc).*loader').test(entry.loader))) {
465+
if (!useEntries.some(entry => entry?.loader && /next.*(babel|swc).*loader/.test(entry.loader))) {
466466
return false;
467467
}
468468

packages/nextjs/src/edge/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export function init(options: VercelEdgeOptions = {}): void {
4848

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

5354
const defaultRewriteFramesIntegration = new RewriteFrames({

packages/nextjs/src/server/index.ts

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

136137
const defaultRewriteFramesIntegration = new RewriteFrames({

packages/sveltekit/src/server/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export function rewriteFramesIteratee(frame: StackFrame): StackFrame {
5151
let strippedFilename;
5252
if (svelteKitBuildOutDir) {
5353
strippedFilename = filename.replace(
54+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input + escaped anyway
5455
new RegExp(`^.*${escapeStringForRegex(join(svelteKitBuildOutDir, 'server'))}/`),
5556
'',
5657
);

packages/sveltekit/src/vite/sourceMaps.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi
139139

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

144145
if (isServerHooksFile) {
@@ -195,6 +196,7 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi
195196
if (fs.existsSync(mapFile)) {
196197
const mapContent = (await fs.promises.readFile(mapFile, 'utf-8')).toString();
197198
const cleanedMapContent = mapContent.replace(
199+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- no user input + escaped anyway
198200
new RegExp(escapeStringForRegex(WRAPPED_MODULE_SUFFIX), 'gm'),
199201
'',
200202
);

packages/tracing-internal/src/node/integrations/express.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ export const extractOriginalRoute = (
412412
const orderedKeys = keys.sort((a, b) => a.offset - b.offset);
413413

414414
// add d flag for getting indices from regexp result
415+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- regexp comes from express.js
415416
const pathRegex = new RegExp(regexp, `${regexp.flags}d`);
416417
/**
417418
* use custom type cause of TS error with missing indices in RegExpExecArray

packages/utils/src/tracing.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { DynamicSamplingContext, PropagationContext, TraceparentData } from
33
import { baggageHeaderToDynamicSamplingContext } from './baggage';
44
import { uuid4 } from './misc';
55

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

0 commit comments

Comments
 (0)