From ea54e56bed6149309ce546013c9eabfd80a17cfb Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Mon, 7 Sep 2026 11:20:47 -0400 Subject: [PATCH] chore(composer): silence Codacy's 18 findings, all of them false positives Checked each one rather than blanket-suppressing. Python (Opengrep, 4). The Jinja2 environment disables autoescaping on purpose -- these templates emit Python, not HTML, and escaping a quote in a plugin name would corrupt the generated source. The safety comes from the values instead (_safe_int, _rgb_expr, _reject_source_breaking), which test_composer_code_injection.py covers. Both the Environment( line and the autoescape= line are reported separately, so each needs its own nosemgrep. The two "Flask route directly returning a formatted string" hits are not routes at all: _as_rgb_filter is a Jinja filter and _rgb_tuple a private helper, both emitting a Python tuple literal with every channel coerced to int first. JavaScript (Biome + ESLint, 14). useQwikValidLexicalScope fired five times on plain arrow-function consts -- it is a Qwik rule about the $() serialization boundary, and this is Alpine.js. noUnusedVariables flagged composerApp(), which the template calls as x-data="composerApp()", where the linter cannot see it. The eight detect-object-injection hits are array indices (this.elements[idx], rawVals[i]) or lookups on module-private maps keyed by an internal element type; none takes an attacker-supplied property name, so disabled per file with the reason rather than eight times inline. .codacy.yml only supports exclude_paths, so these have to be inline. Matches the repo's existing "eslint-disable-line -- " form. 372 composer tests pass, including the 14 JS contract tests that parse these two files. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9 --- web_interface/blueprints/composer.py | 14 ++++++++++---- .../static/v3/js/composer/composer-app.js | 9 +++++++++ .../static/v3/js/composer/composer-canvas.js | 9 +++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/web_interface/blueprints/composer.py b/web_interface/blueprints/composer.py index 843e620a..6f9d68ee 100644 --- a/web_interface/blueprints/composer.py +++ b/web_interface/blueprints/composer.py @@ -69,7 +69,7 @@ def _get_jinja_env() -> jinja2.Environment: global _jinja_env if _jinja_env is None: template_dir = Path(__file__).parent.parent / 'templates' / 'v3' / 'composer' - _jinja_env = jinja2.Environment( # nosec B701 - see below + _jinja_env = jinja2.Environment( # nosec B701 - see below # nosemgrep loader=jinja2.FileSystemLoader(str(template_dir)), # These templates emit Python source, not HTML. Autoescaping would # turn a quote in a plugin name into " inside generated code @@ -79,7 +79,9 @@ def _get_jinja_env() -> jinja2.Environment: # string literal is rejected by _reject_source_breaking. Both are # covered by test/test_composer_code_injection.py, which is where # to look before relaxing any of it. - autoescape=False, + autoescape=False, # nosemgrep - deliberate; these templates + # emit Python, not HTML, and the safety comes from the values + # (_safe_int / _rgb_expr / _reject_source_breaking) instead. trim_blocks=True, lstrip_blocks=True, ) @@ -92,7 +94,9 @@ def _as_rgb_filter(val) -> str: """[r, g, b] → '(r, g, b)'""" if val is None: return 'None' - return f'({int(val[0])}, {int(val[1])}, {int(val[2])})' + # nosemgrep: not a Flask route -- a Jinja filter emitting a Python + # tuple literal, with every channel coerced by int(). + return f'({int(val[0])}, {int(val[1])}, {int(val[2])})' # nosemgrep def _as_fill_filter(val) -> str: @@ -177,7 +181,9 @@ def _rgb_tuple(el: dict, keys, defaults) -> str: source, so they were an injection route exactly like an uncoerced dimension. Every channel now goes through _safe_int. """ - return "(" + ", ".join( + # nosemgrep: not a Flask route -- a private helper emitting a Python + # tuple literal, with every channel clamped to 0-255 by _safe_int. + return "(" + ", ".join( # nosemgrep str(_safe_int(el.get(k), d, 0, 255)) for k, d in zip(keys, defaults) ) + ")" diff --git a/web_interface/static/v3/js/composer/composer-app.js b/web_interface/static/v3/js/composer/composer-app.js index 01e6767f..170203a8 100644 --- a/web_interface/static/v3/js/composer/composer-app.js +++ b/web_interface/static/v3/js/composer/composer-app.js @@ -7,6 +7,12 @@ * - localStorage autosaves on every mutation (debounced 1.5s) * - composer_version in payload allows future server-side migration */ +/* eslint-disable security/detect-object-injection -- + Every hit in this file is either an array index (this.elements[idx], + rawVals[i]) or a lookup on a module-private map keyed by an internal + element type (_HANDLE_CURSORS, ELEMENT_DEFAULTS, previewValues). None of + them takes an attacker-supplied property name, so none can reach a + prototype. Disabled per file rather than eight times inline. */ // ── Template library ───────────────────────────────────────────────────────── const COMPOSER_TEMPLATES = [ @@ -186,6 +192,7 @@ function _debouncedAutosave(payload) { } // ── Main component ─────────────────────────────────────────────────────────── +// biome-ignore lint/correctness/noUnusedVariables: called from the template as x-data="composerApp()" (composer.html), which the linter cannot see. function composerApp() { return { // ── Plugin metadata ─────────────────────────────────────────────── @@ -836,7 +843,9 @@ function composerApp() { x = Math.round(x / this.snapSize) * this.snapSize; y = Math.round(y / this.snapSize) * this.snapSize; } + // biome-ignore lint/correctness/useQwikValidLexicalScope: not Qwik -- this is an Alpine.js component, and the rule is about Qwik's $() serialization boundary, which does not exist here. const clampX = v => Math.max(-this.MATRIX_W, Math.min(this.MATRIX_W * 2, v)); + // biome-ignore lint/correctness/useQwikValidLexicalScope: not Qwik -- this is an Alpine.js component, and the rule is about Qwik's $() serialization boundary, which does not exist here. const clampY = v => Math.max(-this.MATRIX_H, Math.min(this.MATRIX_H * 2, v)); if (el.type === 'line') { el.x0 = clampX(x); el.y0 = clampY(y); diff --git a/web_interface/static/v3/js/composer/composer-canvas.js b/web_interface/static/v3/js/composer/composer-canvas.js index b07c26b8..c44940dc 100644 --- a/web_interface/static/v3/js/composer/composer-canvas.js +++ b/web_interface/static/v3/js/composer/composer-canvas.js @@ -15,6 +15,12 @@ * * Resize handles: drawn on selected rectangles; 8 handles (corners + edge mids). */ +/* eslint-disable security/detect-object-injection -- + Every hit in this file is either an array index (this.elements[idx], + rawVals[i]) or a lookup on a module-private map keyed by an internal + element type (_HANDLE_CURSORS, ELEMENT_DEFAULTS, previewValues). None of + them takes an attacker-supplied property name, so none can reach a + prototype. Disabled per file rather than eight times inline. */ window.ComposerCanvas = (() => { 'use strict'; @@ -318,6 +324,7 @@ window.ComposerCanvas = (() => { } // Helper: compute draw X for text alignment + // biome-ignore lint/correctness/useQwikValidLexicalScope: not Qwik -- this is an Alpine.js component, and the rule is about Qwik's $() serialization boundary, which does not exist here. const _textX = (text, finfo) => { const tw = text.length * finfo.charW * s; if (el.textAlign === 'center') return ax * s - tw / 2; @@ -346,6 +353,7 @@ window.ComposerCanvas = (() => { const key = el.binding?.key || '?'; const pv = opts.previewValues?.[key]; // Substitute {variable} tokens in text using previewValues + // biome-ignore lint/correctness/useQwikValidLexicalScope: not Qwik -- this is an Alpine.js component, and the rule is about Qwik's $() serialization boundary, which does not exist here. const _subVars = str => (str || '').replace(/\{(\w+)\}/g, (_, k) => { const v = opts.previewValues?.[k]; return v !== undefined && v !== '' ? String(v) : `{${k}}`; @@ -552,6 +560,7 @@ window.ComposerCanvas = (() => { ? Math.max(0, Math.min(100, parseFloat(pvGauge) || 0)) / 100 : Math.max(0, Math.min(100, el.previewPct ?? 65)) / 100; const fillSweep = totalSweep * pct; + // biome-ignore lint/correctness/useQwikValidLexicalScope: not Qwik -- this is an Alpine.js component, and the rule is about Qwik's $() serialization boundary, which does not exist here. const toRad = deg => (deg - 90) * Math.PI / 180; // canvas 0=top, PIL 0=right → offset -90 // Track arc