hermes: reject regex literals in polkit grants
A regex literal embedding a quote desynced the string tokenizer and swallowed an unconditional Result.YES, so the grant read as unscoped and slipped past the literal-identity check. Fail closed on any slash outside a string or comment; neither regex nor division belongs in an identity-scoped grant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5020e49057
commit
6e9e8c6ab5
@ -85,6 +85,13 @@ def _split_strings(text: str) -> tuple[str, list[str]]:
|
||||
code.append(" ")
|
||||
index = closing + 2
|
||||
continue
|
||||
if character == "/":
|
||||
# Any remaining slash is a regex literal or a division operator.
|
||||
# A regex literal may embed a quote character, which would desync
|
||||
# the string scanner and swallow an unconditional grant. Neither
|
||||
# regex nor division has a place in an identity-scoped grant, so
|
||||
# both fail closed before they can hide a Result.YES.
|
||||
raise HardeningError("polkit rule uses a regex or division operator")
|
||||
code.append(character)
|
||||
index += 1
|
||||
return "".join(code), strings
|
||||
|
||||
@ -124,6 +124,62 @@ def test_unprovable_rule_constructs_fail_closed(text: str, match: str):
|
||||
_rules(module, text)
|
||||
|
||||
|
||||
def test_regex_literal_tokenizer_desync_grant_is_rejected():
|
||||
"""A regex literal embedding a quote must not open a phantom string.
|
||||
|
||||
Without regex awareness the two /'/ literals fool the string scanner into
|
||||
swallowing the unconditional Result.YES between their quotes; the grant
|
||||
then reads as unscoped and would slip past every later check.
|
||||
"""
|
||||
module = _load()
|
||||
exploit = (
|
||||
"polkit.addRule(function(action, subject) { "
|
||||
"var a = /'/; "
|
||||
'if (action.id == "org.freedesktop.policykit.exec") '
|
||||
"{ return polkit.Result.YES; } "
|
||||
"var b = /'/; });\n"
|
||||
)
|
||||
with pytest.raises(module.HardeningError, match="regex or division"):
|
||||
_rules(module, exploit)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"snippet",
|
||||
[
|
||||
"var a = /'/;",
|
||||
'var a = /"/;',
|
||||
"var a = /abc/;",
|
||||
"var a = /[/'\"]/;",
|
||||
"var a = /x\\/y/;",
|
||||
"var a = subject.user / 2;",
|
||||
"// a comment / with a slash then\nvar a = /'/;",
|
||||
'/* block */ var a = /"/;',
|
||||
"return polkit.Result.YES / 1;",
|
||||
],
|
||||
)
|
||||
def test_every_slash_form_outside_a_comment_fails_closed(snippet: str):
|
||||
module = _load()
|
||||
text = f"polkit.addRule(function(action, subject) {{ {snippet} }});\n"
|
||||
with pytest.raises(module.HardeningError, match="regex or division"):
|
||||
_rules(module, text)
|
||||
|
||||
|
||||
def test_slashes_confined_to_strings_and_comments_stay_legal():
|
||||
module = _load()
|
||||
# Slashes inside string literals and // or /* */ comments are inert.
|
||||
_rules(
|
||||
module,
|
||||
"// path-like /usr/bin comment\n"
|
||||
"/* another /etc/ comment */\n"
|
||||
"polkit.addRule(function(action, subject) {\n"
|
||||
' if (action.id == "org.freedesktop.systemd1.manage-units" &&\n'
|
||||
' subject.user == "systemd-network") {\n'
|
||||
" return polkit.Result.YES;\n"
|
||||
" }\n"
|
||||
"});\n",
|
||||
)
|
||||
|
||||
|
||||
def test_string_and_comment_handling_keeps_literal_rules_auditable():
|
||||
module = _load()
|
||||
code, strings = module._split_strings(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user