diff --git a/.github/workflows/semantic-pull-request.yaml b/.github/workflows/semantic-pull-request.yaml index 7ea45395a..b45a1c840 100644 --- a/.github/workflows/semantic-pull-request.yaml +++ b/.github/workflows/semantic-pull-request.yaml @@ -53,6 +53,7 @@ jobs: prompt knowledge plugin + security middleware model database diff --git a/frontend/packages/project-ide/core/src/common/__tests__/context-key-service.test.ts b/frontend/packages/project-ide/core/src/common/__tests__/context-key-service.test.ts new file mode 100644 index 000000000..e949d6a9f --- /dev/null +++ b/frontend/packages/project-ide/core/src/common/__tests__/context-key-service.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2025 coze-dev Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'reflect-metadata'; +import { ContextKeyService, ContextKey } from '../context-key-service'; + +describe('ContextKeyService', () => { + let service: ContextKeyService; + + beforeEach(() => { + service = new ContextKeyService(); + }); + + describe('basic functionality', () => { + it('should set and get context values', () => { + service.setContext('testKey', true); + expect(service.getContext('testKey')).toBe(true); + }); + + it('should have default editorFocus context', () => { + expect(service.getContext(ContextKey.editorFocus)).toBe(true); + }); + }); + + describe('expression matching', () => { + beforeEach(() => { + service.setContext('active', true); + service.setContext('visible', false); + }); + + it('should match simple boolean expressions', () => { + expect(service.match('active')).toBe(true); + expect(service.match('visible')).toBe(false); + }); + + it('should match complex boolean expressions', () => { + expect(service.match('active && visible')).toBe(false); + expect(service.match('active || visible')).toBe(true); + expect(service.match('!visible')).toBe(true); + }); + + it('should handle unknown context keys safely', () => { + expect(service.match('unknownKey')).toBe(false); + expect(service.match('active && unknownKey')).toBe(false); + }); + }); + + describe('security', () => { + it('should reject malicious expressions', () => { + const maliciousExpressions = [ + 'alert("xss")', + 'console.log("test")', + 'process.exit(0)', + 'require("fs")', + 'new Function("alert(1)")()', + 'eval("1+1")', + 'window.location = "evil.com"', + 'document.createElement("script")', + '(() => { alert(1); })()', + 'active; alert(1)', + ]; + + maliciousExpressions.forEach(expr => { + expect(service.match(expr)).toBe(false); + }); + }); + + it('should only allow safe boolean operations', () => { + service.setContext('key1', true); + service.setContext('key2', false); + + const safeExpressions = [ + 'key1', + 'key1 && key2', + 'key1 || key2', + '!key1', + 'key1 == key2', + 'key1 != key2', + 'key1 === key2', + 'key1 !== key2', + ]; + + safeExpressions.forEach(expr => { + expect(() => service.match(expr)).not.toThrow(); + }); + }); + + it('should handle edge cases gracefully', () => { + expect(service.match('')).toBe(false); + expect(service.match(' ')).toBe(false); + expect(service.match('123')).toBe(false); + expect(service.match('true')).toBe(true); // 'true' is a boolean literal + expect(service.match('false')).toBe(false); // 'false' is a boolean literal + }); + }); +}); diff --git a/frontend/packages/project-ide/core/src/common/context-key-service.ts b/frontend/packages/project-ide/core/src/common/context-key-service.ts index cccfee6bd..1dd00153c 100644 --- a/frontend/packages/project-ide/core/src/common/context-key-service.ts +++ b/frontend/packages/project-ide/core/src/common/context-key-service.ts @@ -52,10 +52,64 @@ export class ContextKeyService implements ContextMatcher { } public match(expression: string): boolean { - const keys = Array.from(this._contextKeys.keys()); - const func = new Function(...keys, `return ${expression};`); - const res = func(...keys.map(k => this._contextKeys.get(k))); + try { + return this.evaluateExpression(expression); + } catch (error) { + console.warn('Invalid context expression:', expression, error); + return false; + } + } + + private evaluateExpression(expression: string): boolean { + const sanitizedExpression = expression.trim(); + + // Allow only safe boolean expressions with context keys + const safeExpressionPattern = + /^!?[a-zA-Z_$][a-zA-Z0-9_$]*(\s*(&&|\|\||==|!=|===|!==)\s*!?[a-zA-Z_$][a-zA-Z0-9_$]*)*$/; + + if (!safeExpressionPattern.test(sanitizedExpression)) { + throw new Error('Unsafe expression detected'); + } + + // Parse and evaluate the expression safely + return this.safeEvaluate(sanitizedExpression); + } + + private safeEvaluate(expression: string): boolean { + // Replace context keys with their actual values + let executableExpression = expression; + + // Track which keys have been replaced to avoid replacing them again + const replacedKeys = new Set(); + + for (const [key, value] of this._contextKeys) { + const regex = new RegExp(`\\b${key}\\b`, 'g'); + // Convert all values to boolean string representation + const boolValue = Boolean(value); + if (executableExpression.includes(key)) { + executableExpression = executableExpression.replace( + regex, + String(boolValue), + ); + replacedKeys.add(key); + } + } + + // Now evaluate the boolean expression safely + // Only allow basic boolean operations + try { + // Remove any remaining unrecognized identifiers (replace with false) + // But don't replace 'true' or 'false' literals + executableExpression = executableExpression.replace( + /\b(?!true|false)[a-zA-Z_$][a-zA-Z0-9_$]*\b/g, + 'false', + ); - return res; + // eslint-disable-next-line no-eval -- Safe after sanitization + return Boolean(eval(executableExpression)); + } catch (error) { + console.warn('Expression evaluation failed:', error); + return false; + } } }