Compare commits
2 Commits
fix/workfl
...
fix/securi
| Author | SHA1 | Date | |
|---|---|---|---|
| 43cb1b8643 | |||
| 6ba9c4b749 |
1
.github/workflows/semantic-pull-request.yaml
vendored
1
.github/workflows/semantic-pull-request.yaml
vendored
@ -53,6 +53,7 @@ jobs:
|
||||
prompt
|
||||
knowledge
|
||||
plugin
|
||||
security
|
||||
middleware
|
||||
model
|
||||
database
|
||||
|
||||
@ -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<boolean>('testKey')).toBe(true);
|
||||
});
|
||||
|
||||
it('should have default editorFocus context', () => {
|
||||
expect(service.getContext<boolean>(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
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
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<string>();
|
||||
|
||||
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',
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-eval -- Safe after sanitization
|
||||
return Boolean(eval(executableExpression));
|
||||
} catch (error) {
|
||||
console.warn('Expression evaluation failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user