Skip to content

[Bug]: allowed_tools bypasses denied_commands and path deny-rules (deny should take precedence) #313

Description

@glitch-ux

Summary

PermissionChecker.evaluate() returns allow for any tool in allowed_tools before it checks the user-configured denied_commands patterns and path_rules deny-rules. As a result, allow-listing a broad tool such as bash or write_file silently disables the user's own deny policy.

This contradicts the design used everywhere else in the checker, where deny takes precedence over allow. Built-in sensitive-path protection and the explicit denied_tools list are both evaluated before the allow-list (and there is even a test, test_allowed_tools_does_not_bypass_sensitive_paths, asserting the allow-list cannot bypass sensitive paths). denied_commands and path deny-rules are the inconsistent exceptions.

Affected code

src/openharness/permissions/checker.py — in evaluate(), the allow-list short-circuit at line 105 returns before the path deny-rule loop (lines 108–117) and the denied_commands loop (lines 119–126):

# Explicit tool allow list
if tool_name in self._settings.allowed_tools:
    return PermissionDecision(allowed=True, reason=f"{tool_name} is explicitly allowed")

# Check path-level rules
if file_path and self._path_rules:
    ...        # deny rules here are never reached for allow-listed tools

# Check command deny patterns (e.g. deny "rm -rf /")
if command:
    ...        # denied_commands here are never reached for allow-listed tools

Reproduction

from openharness.config.settings import PathRuleConfig, PermissionSettings
from openharness.permissions import PermissionChecker, PermissionMode

# 1) denied_commands bypassed by allow-listing `bash`
s = PermissionSettings.model_construct(
    mode=PermissionMode.DEFAULT, allowed_tools=["bash"], denied_tools=[],
    denied_commands=["*rm -rf /*"], path_rules=[],
)
print(PermissionChecker(s).evaluate(
    "bash", is_read_only=False, command="rm -rf / --no-preserve-root").allowed)
# expected: False  (matches deny pattern)   ->   actual: True

# 2) path deny-rule bypassed by allow-listing `write_file`
s2 = PermissionSettings.model_construct(
    mode=PermissionMode.DEFAULT, allowed_tools=["write_file"], denied_tools=[],
    denied_commands=[], path_rules=[PathRuleConfig(pattern="/etc/*", allow=False)],
)
print(PermissionChecker(s2).evaluate(
    "write_file", is_read_only=False, file_path="/etc/passwd").allowed)
# expected: False  (matches deny rule)       ->   actual: True

Both print True. With allowed_tools empty, the same calls are correctly denied, confirming the allow-list short-circuit is the cause.

Impact

Users commonly allow-list bash (or another mutating tool) for a smoother session while relying on denied_commands / path_rules as guardrails against destructive or sensitive operations — exactly the configuration where this matters most. Because the LLM drives tool calls, this also weakens defense-in-depth against prompt-injection-directed destructive commands. Severity is high: a configured deny rule provides no protection once the tool is allow-listed.

Expected behavior

Deny rules should take precedence over the allow-list (as they already do for denied_tools and built-in sensitive paths). denied_commands and path_rules deny-rules should be evaluated before the allowed_tools short-circuit.

Environment

  • OpenHarness main (v0.1.9)
  • Python 3.10 / 3.11

I'll open a PR that reorders the deny checks ahead of the allow-list and adds regression tests.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions