Compare commits
2 Commits
feat/plugi
...
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
|
prompt
|
||||||
knowledge
|
knowledge
|
||||||
plugin
|
plugin
|
||||||
|
security
|
||||||
middleware
|
middleware
|
||||||
model
|
model
|
||||||
database
|
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 {
|
public match(expression: string): boolean {
|
||||||
const keys = Array.from(this._contextKeys.keys());
|
try {
|
||||||
const func = new Function(...keys, `return ${expression};`);
|
return this.evaluateExpression(expression);
|
||||||
const res = func(...keys.map(k => this._contextKeys.get(k)));
|
} 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