fix(core): prevent malformed permission rules from becoming tool-wide catch-alls (#3467)

* fix(core): prevent malformed permission rules from becoming tool-wide catch-alls

A permission rule with unbalanced parentheses (e.g. `Bash(rm -rf /)*`)
was silently parsed with `specifier: undefined`, causing `matchesRule`
to treat it as a catch-all that matches every invocation of the tool.
For deny rules this blocked all commands; for allow rules a typo could
silently auto-approve everything.

Add an `invalid` flag to `PermissionRule`. `parseRule` now marks rules
with unbalanced parens as invalid, `matchesRule` short-circuits them to
never match, and all entry points (`addSession*Rule`, `addPersistentRule`,
`parseRules`) warn on malformed input. `listRules` filters out invalid
rules so they don't appear in the /permissions UI.

* fix(cli): show error in /permissions dialog when adding malformed rule

When a user enters a rule with unbalanced parentheses via the "Add Rule"
input in the /permissions dialog, show an inline error message instead of
silently accepting and then hiding the invalid rule.

Closes #3459

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
jinye 2026-04-20 18:56:14 +08:00 committed by GitHub
parent c74d7678cb
commit bf561fa495
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 145 additions and 9 deletions

View file

@ -24,7 +24,7 @@ import type {
RuleWithSource,
RuleType,
} from '@qwen-code/qwen-code-core';
import { isPathWithinRoot } from '@qwen-code/qwen-code-core';
import { isPathWithinRoot, parseRule } from '@qwen-code/qwen-code-core';
// ---------------------------------------------------------------------------
// Types
@ -164,6 +164,7 @@ export function PermissionsDialog({
// --- Dialog view state machine ---
const [view, setView] = useState<DialogView>('rule-list');
const [newRuleInput, setNewRuleInput] = useState('');
const [ruleInputError, setRuleInputError] = useState('');
const [pendingRuleText, setPendingRuleText] = useState('');
const [deleteTarget, setDeleteTarget] = useState<RuleWithSource | null>(null);
@ -455,6 +456,7 @@ export function PermissionsDialog({
(value: string) => {
if (value === '__add__') {
setNewRuleInput('');
setRuleInputError('');
setView('add-rule-input');
return;
}
@ -471,6 +473,16 @@ export function PermissionsDialog({
const handleAddRuleSubmit = useCallback(() => {
const trimmed = newRuleInput.trim();
if (!trimmed) return;
const rule = parseRule(trimmed);
if (rule.invalid) {
setRuleInputError(
t(
'Malformed rule: unbalanced parentheses. Use the format ToolName(specifier).',
),
);
return;
}
setRuleInputError('');
setPendingRuleText(trimmed);
setView('add-rule-scope');
}, [newRuleInput]);
@ -812,6 +824,12 @@ export function PermissionsDialog({
isActive={true}
/>
</Box>
{ruleInputError && (
<>
<Box height={1} />
<Text color={theme.status.error}>{ruleInputError}</Text>
</>
)}
</Box>
<Box marginTop={1} marginLeft={1}>
<Text color={theme.text.secondary}>