ci(security): integrate Bright CI pipeline for security tests and remediation#911
ci(security): integrate Bright CI pipeline for security tests and remediation#911bright-security-golf[bot] wants to merge 19 commits intostablefrom
Conversation
skip-checks:true
skip-checks:true
skip-checks:true
skip-checks:true
skip-checks:true
skip-checks:true
| const text = raw.toString().trim(); | ||
| const res = dotT.compile(text)(); | ||
| // Use a safe template rendering approach | ||
| const res = dotT.template(text, { evaluate: false, interpolate: false, encode: false, use: false, define: false, varname: 'it' })({}); |
Check failure
Code scanning / CodeQL
Code injection Critical
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
In general, to fix code/ template injection issues, do not compile or evaluate user-controlled strings as templates or code. Instead, keep the template static and treat user data as values injected into placeholders. If the feature is meant to just echo or transform text, use straightforward string handling (e.g., return the text, escape it, or apply safe formatting), avoiding template engines that support execution of embedded code.
For this concrete case, the best fix without changing observable behavior too much is to stop treating raw as a doT template at all. The endpoint is described as “Write your text here” and returns “Rendered result”, but there is no indication that users are meant to write doT syntax. The simplest secure behavior is to take the incoming text, trim it, and return it directly. That removes the template compilation entirely and thus eliminates the sink CodeQL flags. Concretely in src/app.controller.ts, within renderTemplate, remove the call to dotT.template and replace it with just returning text. Also log the raw text instead of a “rendered template.” No new methods or imports are needed; the existing dotT import can remain (or be cleaned up separately if the project no longer uses it elsewhere, but that’s outside the shown snippet).
| @@ -76,10 +76,8 @@ | ||
| async renderTemplate(@Body() raw): Promise<string> { | ||
| if (typeof raw === 'string' || Buffer.isBuffer(raw)) { | ||
| const text = raw.toString().trim(); | ||
| // Use a safe template rendering approach | ||
| const res = dotT.template(text, { evaluate: false, interpolate: false, encode: false, use: false, define: false, varname: 'it' })({}); | ||
| this.logger.debug(`Rendered template: ${res}`); | ||
| return res; | ||
| this.logger.debug(`Received text to render: ${text}`); | ||
| return text; | ||
| } | ||
| } | ||
|
|
| try { | ||
| const result = eval(processNumbersExpression); | ||
| // Use Function constructor to safely evaluate the expression | ||
| const func = new Function('numbers', `return ${processNumbersExpression}`); |
Check failure
Code scanning / CodeQL
Code injection Critical
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
In general, the way to fix this issue is to stop evaluating user-provided strings as code. Instead, define a limited set of supported operations (for example, “sum”, “average”, “max”, “min”) or implement a safe, non-Turing-complete expression evaluator that parses and interprets expressions without using eval, Function, or similar dynamic-code features. The user input should only select among pre-defined behavior or be parsed as data, not executed.
For this specific endpoint, the safest fix that preserves existing functionality as much as possible is to:
- Define a small set of allowed processing “modes” that cover the intended use (e.g.,
sum,average,max,min,count). - Interpret
payload.processing_expressionas one of these modes rather than as a raw JavaScript expression. - Remove the
new Functionusage and compute the result based on the selected mode. - Keep the default behavior the same as today (sum) when no or unknown expression is provided.
Concretely, in src/app.controller.ts inside processNumbers (around lines 202–247):
- Replace the string-typed
processing_expressionwith logic that maps allowed string values (like"sum","avg","max","min","count") to fixed computations onnumbers. - Replace the creation and invocation of
new Functionwith aswitch/ifblock implementing these computations. - Keep the rest of the response handling (status codes, content types, error handling) the same.
- No new imports are strictly necessary; we can implement the computations inline.
This removes the code-injection sink while preserving the main behavior (processing arrays of numbers according to a client-chosen mode, defaulting to sum).
| @@ -205,22 +205,53 @@ | ||
| @Res() res: FastifyReply | ||
| ): Promise<void> { | ||
| const numbers = Array.isArray(payload?.numbers) ? payload.numbers : []; | ||
| const processNumbersExpression = | ||
| typeof payload?.processing_expression === 'string' && | ||
| payload.processing_expression.trim().length > 0 | ||
| ? payload.processing_expression | ||
| : 'numbers.reduce((acc, num) => acc + num, 0)'; | ||
| const processingExpressionRaw = | ||
| typeof payload?.processing_expression === 'string' | ||
| ? payload.processing_expression.trim() | ||
| : ''; | ||
|
|
||
| // Map the user-supplied processing_expression to a supported operation. | ||
| // Default behavior remains summing all numbers. | ||
| const processingMode = | ||
| processingExpressionRaw.length > 0 ? processingExpressionRaw.toLowerCase() : 'sum'; | ||
|
|
||
| // expose both names used by exploiter payloads | ||
| const response = res; | ||
|
|
||
| this.logger.debug(`Processing crystals with ${numbers.length} values`); | ||
| this.logger.debug(`Processing crystals with ${numbers.length} values using mode "${processingMode}"`); | ||
|
|
||
| try { | ||
| // Use Function constructor to safely evaluate the expression | ||
| const func = new Function('numbers', `return ${processNumbersExpression}`); | ||
| const result = func(numbers); | ||
| let result: unknown; | ||
|
|
||
| // Implement a small, fixed set of supported numeric operations. | ||
| switch (processingMode) { | ||
| case 'sum': | ||
| result = numbers.reduce((acc, num) => acc + num, 0); | ||
| break; | ||
| case 'avg': | ||
| case 'average': | ||
| if (numbers.length === 0) { | ||
| result = 0; | ||
| } else { | ||
| const sum = numbers.reduce((acc, num) => acc + num, 0); | ||
| result = sum / numbers.length; | ||
| } | ||
| break; | ||
| case 'max': | ||
| result = numbers.length > 0 ? Math.max(...numbers) : null; | ||
| break; | ||
| case 'min': | ||
| result = numbers.length > 0 ? Math.min(...numbers) : null; | ||
| break; | ||
| case 'count': | ||
| result = numbers.length; | ||
| break; | ||
| default: | ||
| // Fallback to the original default behavior (sum) for unknown modes. | ||
| result = numbers.reduce((acc, num) => acc + num, 0); | ||
| break; | ||
| } | ||
|
|
||
| // SSJI payload may already end the response | ||
| if (response.sent || response.raw.writableEnded) { | ||
| return; |
| throw new Error('cannot delete file from this location'); | ||
| } else { | ||
| file = path.resolve(process.cwd(), file); | ||
| await fs.promises.unlink(file); |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 days ago
To fix the problem, we need to ensure that the file path used in fs.promises.unlink is constrained to a safe, intended directory tree and properly normalized. This is similar to how getFile uses isValidPath and path.resolve to limit reads to allowed directories. For deletions, we should reuse the same validation logic and only proceed if the resolved path is within one of the permitted directories, rejecting any other paths.
Concretely, in src/file/file.service.ts, we should:
- Reuse
isValidPathinsidedeleteFilethe same way it’s used ingetFile:- Call
this.isValidPath(file)early indeleteFile. - If it returns
false, throw aBadRequestExceptionindicating that the path is not allowed.
- Call
- Normalize the path in a controlled way before deletion:
- Resolve
filerelative toprocess.cwd()(as already done). - Optionally, we can rely on
isValidPath’spath.resolvefor the base check; but to keep behavior consistent withgetFile, we’ll resolve once indeleteFileafter the validation passes.
- Resolve
- Keep the existing checks denying absolute paths and HTTP URLs, since those are stricter constraints and likely part of the intended behavior.
We don’t need new imports: BadRequestException is already imported, and path and fs are already used. All changes are localized to the deleteFile method body in src/file/file.service.ts. The file.controller.ts file does not require changes for this specific issue.
| @@ -48,6 +48,11 @@ | ||
|
|
||
| async deleteFile(file: string): Promise<boolean> { | ||
| try { | ||
| // Ensure the provided path is within an allowed directory | ||
| if (!this.isValidPath(file)) { | ||
| throw new BadRequestException('Access to the specified file path is not allowed.'); | ||
| } | ||
|
|
||
| if (file.startsWith('/')) { | ||
| throw new Error('cannot delete file from this location'); | ||
| } else if (file.startsWith('http')) { |
Note
Fixed 19 of 21 vulnerabilities.
Please review the fixes before merging.
GET /api/partners/partnerLoginPOST /api/renderdotlibrary to prevent code execution from user input.POST /graphqlPOST /api/process_numbersevalwithFunctionconstructor to safely evaluate the processing expression.GET /api/partners/searchPartnersGET /api/fileGET /api/file/digital_oceanGET /api/file/googleGET /api/fileGET /api/fileGET /api/file/awsGET /api/file/azureGET /api/users/id/1GET /api/secretsGET /api/testimonials/countPOST /graphqlDELETE /api/fileGET /api/configPOST /graphqlGET /api/products/latestWorkflow execution details