mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 06:45:09 +00:00
fix(composer): close binding_source code injection, line-align, and project_root UnboundLocalError
Three findings from CodeRabbit's review of 6c23994b, all verified against
current code before fixing:
- manager.py.j2 interpolated binding.source unescaped into a Python comment
(`pass # dynamic_text binding_source "{{ el.binding_source }}" draws
nothing`). A source string with a newline broke out of the comment; a
crafted payload produces a clean, ast.parse-valid `import os` in the
generated plugin (confirmed against the pre-fix template). This is now a
fixed literal comment that never interpolates the value. Live now that
composer_bp is registered. CWE-94.
- _alignElement moved a line's x0 (or y0) to the new position but left x1
(or y1) behind, so aligning a line changed its shape instead of moving
it. Both endpoints now translate by the same delta.
- web_interface/app.py only assigned project_root inside the relative-path
branch of the plugins_dir resolution. An absolute plugin_system.plugins_
directory (a supported config value) hit UnboundLocalError importing the
module at all, since SchemaManager/composer_bp use project_root further
down. Now assigned unconditionally before the branch.
Also extends BOUND_TYPES coverage in composer-app.js (_isBound,
removeConfigVar, _validateBeforeExport) from dynamic_text/progress_bar to
all six element types that carry a binding object (countdown, pips,
sparkline, gauge too) -- found by direct code reading against
ELEMENT_DEFAULTS in composer-canvas.js, not from a review comment. Without
it, those four types could export with an unbound config key with no
validation error, and deleting a config var they used gave no warning.
All four fixes have mutation-checked regression tests (fail against the
reverted code, pass with the fix): test_binding_source_cannot_break_out_of_the_comment_it_lands_in,
test_align_translates_both_line_endpoints_not_just_the_start,
test_app_plugins_dir_resolution.py, test_binding_checks_cover_every_bound_element_type.
Full suite: 4365 passed, 58 skipped, 2 failed -- both the pre-existing
Europe/Kiev/Asia/Calcutta tzdata-alias gap on this sandbox, identical on
origin/main, unrelated to this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ has no branch for, and the template emits a `pass` fallback so a type added to
|
||||
the canvas before its branch exists degrades to a no-op instead of a broken
|
||||
plugin.
|
||||
"""
|
||||
import ast
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -98,4 +99,26 @@ def test_dynamic_text_with_non_config_binding_does_not_break_generation(wrapper)
|
||||
"binding": {"source": "live", "key": "temperature"}, **wrapper}
|
||||
files = generate(element) # must not raise ComposerInputError
|
||||
assert "manager.py" in files
|
||||
assert 'binding_source "live" draws nothing' in files["manager.py"]
|
||||
assert "non-config dynamic_text binding draws nothing" in files["manager.py"]
|
||||
|
||||
|
||||
def test_binding_source_cannot_break_out_of_the_comment_it_lands_in():
|
||||
"""binding.source used to be interpolated straight into a Python comment
|
||||
(`pass # dynamic_text binding_source "{{ el.binding_source }}" draws
|
||||
nothing`) with no escaping. A source string carrying a newline closed the
|
||||
comment, and text on the following line(s), indented to match, became a
|
||||
real statement in the generated plugin -- CWE-94, and live once
|
||||
composer_bp was registered (confirmed: this exact payload produces a
|
||||
manager.py containing a clean, ast.parse-valid `import os` against the
|
||||
pre-fix template). The comment is now a fixed literal that never
|
||||
interpolates the value at all.
|
||||
"""
|
||||
payload = "foo\n import os\n os.system('id') # "
|
||||
element = {"type": "dynamic_text", "x": 0, "y": 0, "color": "#ffffff",
|
||||
"binding": {"source": payload, "key": "temperature"}}
|
||||
files = generate(element)
|
||||
src = files["manager.py"]
|
||||
assert "import os" not in src
|
||||
assert "os.system" not in src
|
||||
assert "non-config dynamic_text binding draws nothing" in src
|
||||
ast.parse(src) # belt and braces: generate() already enforces this
|
||||
|
||||
Reference in New Issue
Block a user