Skip to content

Commit 1cbaaf5

Browse files
fix: Escape translated catalog strings before rendering as HTML in settings UI (homeassistant-ai#1931)
* fix: escape translated catalog strings before rendering as HTML in settings UI Resolves CodeQL js/xss-through-dom alerts 33-40. Translation catalog values were interpolated raw into innerHTML so they could carry inline markup, making the community-edited locale catalogs an injection surface. New tHtml() escapes the whole catalog string and restores only the allowlisted formatting tags (<code>, <strong>, and the internal tab links); placeholders are substituted after escaping with caller-built, pre-escaped fragments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: gate PRs on the default CodeQL security suites alongside quality GitHub default setup only analyzes master post-merge and posts no blocking check on PRs, so security-query findings surface as code-scanning alerts only after merge. Run the default <language>-code-scanning suites through the existing quality gate so the same queries block the PR instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: resolve the security-suite findings surfaced by the strengthened gate - settings.js: escape the three remaining raw field-name interpolations in feature-row label ids (residual js/xss-through-dom flows). - webhook-proxy dev v1.2.3.dev8: write the proxy-config handoff file via _atomic_write_0600 like the creds file (py/clear-text-storage); stable gets the same via the promote workflow. - ui_panel: rewrite the locale-cookie regex to an unambiguous linear form (py/polynomial-redos); same accepted language, no version bump needed (rides pending 1.2.0). - codeql_quality_gate.py: allowlist the verified false positives and reviewed-by-design patterns (masked client-id logging, SHA256 identity fingerprints, admin-only connect logs, 0600 warned fallbacks, test assertions and the test-env credential printout), each with its rationale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: replace the locale-cookie regex with a linear character-walk validator CodeQL's backtracking model flags every regex shape for this language as potentially polynomial; a plain walk is linear by construction and accepts the identical language (verified exhaustively to length 6). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: pin the review-found gaps — catalog markup validation, proxy-config 0600, locale validator - _i18n.py: reject catalog messages whose inline markup tHtml cannot restore (exact <code>/<strong>/panel-link shapes), mirroring the placeholder-parity load-time check; a deviating translation previously rendered as literal escaped text with no signal. - tests/addon/test_webhook_proxy.py: assert the proxy-config handoff file lands on disk (0600 on the dev flavor) and exercise the warned plain-write fallback branch. - tests/src/unit/test_ui_panel.py: parametrized coverage of the locale cookie validator's separator and ASCII rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: base the webhook-proxy dev version on the current stable line Dev was still counting from the 1.2.2 fork point (1.2.3.devN) while stable moved to 2.0.2, so dev sorted BEHIND the stable it promotes into. Re-base to 2.0.3.dev1 (next stable patch + .devN) and record the rule in the shared AGENTS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: kingpanther13 <kingpanther13@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 52bd77c commit 1cbaaf5

14 files changed

Lines changed: 528 additions & 34 deletions

File tree

.github/workflows/codeql-quality.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
name: CodeQL Code Quality
22

3-
# Runs the CodeQL "<language>-code-quality" suites and fails the PR on ANY finding.
3+
# Runs the CodeQL "<language>-code-quality" suite PLUS the default
4+
# "<language>-code-scanning" security suite per language and fails the PR on
5+
# ANY finding.
6+
#
7+
# Why the security suite is gated here too: GitHub default setup (the
8+
# Security tab) only analyzes master after merge and posts no blocking check
9+
# on PRs, so a PR can introduce security-query findings (e.g.
10+
# js/xss-through-dom) that surface as code-scanning alerts minutes after
11+
# merge — that is how alerts 33-40 landed. Running the same default
12+
# code-scanning suite in this gate catches them pre-merge.
413
#
514
# Languages gated (one matrix leg each), both scanning the whole tree
615
# (--source-root .):
@@ -68,8 +77,10 @@ jobs:
6877
include:
6978
- language: python
7079
suite: codeql/python-queries:codeql-suites/python-code-quality.qls
80+
security_suite: codeql/python-queries:codeql-suites/python-code-scanning.qls
7181
- language: javascript
7282
suite: codeql/javascript-queries:codeql-suites/javascript-code-quality.qls
83+
security_suite: codeql/javascript-queries:codeql-suites/javascript-code-scanning.qls
7384

7485
steps:
7586
- uses: actions/checkout@v7
@@ -96,12 +107,13 @@ jobs:
96107
--source-root . \
97108
--overwrite
98109
99-
- name: Analyze with ${{ matrix.language }}-code-quality suite
110+
- name: Analyze with ${{ matrix.language }} quality + security suites
100111
env:
101112
GH_TOKEN: ${{ github.token }}
102113
run: |
103114
gh codeql database analyze ha-mcp-db \
104115
${{ matrix.suite }} \
116+
${{ matrix.security_suite }} \
105117
--format=sarif-latest \
106118
--output quality.sarif \
107119
--download

custom_components/ha_mcp_tools/ui_panel.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
from __future__ import annotations
4848

4949
import logging
50-
import re
5150
import secrets
5251
import time
5352
from typing import TYPE_CHECKING, Any
@@ -86,7 +85,28 @@
8685
# Keep in sync with ``ha_mcp.settings_ui._i18n.LOCALE_COOKIE`` without
8786
# importing the separately installed server package into the HA component.
8887
_LOCALE_COOKIE_NAME = "ha_mcp_locale"
89-
_LOCALE_COOKIE_VALUE_RE = re.compile(r"[A-Za-z0-9]+(?:[-_][A-Za-z0-9]+)*\Z")
88+
89+
90+
def _is_valid_locale_cookie_value(value: str) -> bool:
91+
"""True for BCP-47-like values: ASCII-alphanumeric runs joined by single
92+
``-``/``_`` separators (no leading/trailing/doubled separators).
93+
94+
A plain character walk instead of a regex: linear by construction, where
95+
CodeQL's backtracking model flagged every regex shape for this language
96+
as potentially polynomial.
97+
"""
98+
prev_is_sep = True # a separator may not open the value
99+
for ch in value:
100+
if ch in "-_":
101+
if prev_is_sep:
102+
return False
103+
prev_is_sep = True
104+
elif ch.isascii() and ch.isalnum():
105+
prev_is_sep = False
106+
else:
107+
return False
108+
return bool(value) and not prev_is_sep
109+
90110

91111
# Session lifetime. Short by design; the panel re-mints well within it while open.
92112
_SESSION_TTL_SECONDS = 8 * 60 * 60
@@ -140,7 +160,7 @@ def _forwarded_locale_cookie(request: web.Request) -> str | None:
140160
if (
141161
not isinstance(value, str)
142162
or len(value) > 64
143-
or _LOCALE_COOKIE_VALUE_RE.fullmatch(value) is None
163+
or not _is_valid_locale_cookie_value(value)
144164
):
145165
return None
146166
return f"{_LOCALE_COOKIE_NAME}={value}"

homeassistant-addon-webhook-proxy-dev/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ history from before the fork.
99
-->
1010

1111

12+
## v2.0.3.dev1 (2026-07-18)
13+
14+
Version line re-based onto the stable series (stable is 2.0.2, so dev now
15+
leads it as 2.0.3.devN); the 1.2.3.devN entries below predate this rule.
16+
17+
### Bug Fixes
18+
19+
- Write the proxy-config handoff file with restricted (0600) permissions like
20+
the OAuth creds file, falling back to a plain write with a logged warning
21+
when the filesystem cannot honor the mode.
22+
23+
1224
## v1.2.3.dev6 (2026-07-05)
1325

1426
### Documentation

homeassistant-addon-webhook-proxy-dev/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: "Nabu Casa - Webhook Proxy for HA MCP (Dev)"
22
description: "DEV CHANNEL (unstable) — remote access proxy via Nabu Casa or any reverse proxy. Cannot run alongside the stable Webhook Proxy add-on."
3-
version: "1.2.3.dev7"
3+
version: "2.0.3.dev1"
44
slug: "ha_mcp_webhook_proxy_dev"
55
url: "https://github.com/homeassistant-ai/ha-mcp"
66
stage: experimental

homeassistant-addon-webhook-proxy-dev/mcp_proxy_dev/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"dependencies": ["webhook"],
88
"documentation": "https://github.com/homeassistant-ai/ha-mcp",
99
"iot_class": "local_push",
10-
"version": "1.2.3.dev7"
10+
"version": "2.0.3.dev1"
1111
}

homeassistant-addon-webhook-proxy-dev/start.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1298,8 +1298,20 @@ def main() -> int:
12981298
if debug_logging:
12991299
proxy_config["debug_logging"] = True
13001300
proxy_config_file = Path("/config/.mcp_proxy_dev_config.json")
1301+
proxy_config_json = json.dumps(proxy_config)
13011302
try:
1302-
proxy_config_file.write_text(json.dumps(proxy_config))
1303+
if not _atomic_write_0600(proxy_config_file, proxy_config_json.encode("utf-8")):
1304+
# False = the restricted-mode create OR the write/replace failed —
1305+
# same degradation semantics as the OAuth creds path above. The
1306+
# file carries the OAuth keys when auth is enabled, so it gets the
1307+
# same 0600-first treatment; a mode-only limitation falls back to
1308+
# a plain write rather than breaking startup.
1309+
proxy_config_file.write_text(proxy_config_json)
1310+
log_error(
1311+
f"Could not create the proxy config file with restricted "
1312+
f"permissions at {proxy_config_file}. It may have wider "
1313+
f"permissions than intended."
1314+
)
13031315
except OSError as e:
13041316
log_error(f"Failed to write proxy config: {e}")
13051317
return 1

homeassistant-addon-webhook-proxy/AGENTS.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,18 @@ is `started`. `start.py:_sibling_is_running` matches the sibling by exact slug o
4949
`_<base>` suffix (Supervisor hash-prefixes third-party slugs).
5050

5151
## Versioning
52-
- Dev: bump `homeassistant-addon-webhook-proxy-dev/config.yaml` `version` AND
52+
- Dev: the version is BASED ON THE CURRENT STABLE version — the next stable
53+
patch with a `.devN` suffix (stable `2.0.2``2.0.3.dev1`, `2.0.3.dev2`, …),
54+
so dev always sorts ahead of the stable it will promote into. Bump
55+
`homeassistant-addon-webhook-proxy-dev/config.yaml` `version` AND
5356
`mcp_proxy_dev/manifest.json` `version` together (they must stay equal). The
5457
`webhook-proxy-dev-version-guard` workflow fails any PR that touches the dev add-on
5558
without an increase. Use the `Webhook Proxy Dev — Bump Version` workflow
5659
(`workflow_dispatch`, never scheduled) to do the bump and open a draft PR, or edit the
57-
two files by hand.
58-
- Stable keeps its own independent version line and never inherits a `.devN` label.
60+
two files by hand. (Dev entries below `v2.0.3.dev1` in the dev CHANGELOG predate this
61+
rule — the line originally counted independently from the 1.2.2 fork point.)
62+
- Stable keeps its own version line and never inherits a `.devN` label; the promote
63+
workflow assigns the stable number.
5964

6065
## Promotion (dev -> stable)
6166
When the dev flavor is ready to become stable, run the `Webhook Proxy — Promote Dev to

scripts/codeql_quality_gate.py

Lines changed: 171 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
#!/usr/bin/env python3
2-
"""Parse a CodeQL code-quality SARIF file, report findings, and gate on them.
2+
"""Parse a CodeQL SARIF file, report findings, and gate on them.
33
44
CodeQL's Code Quality preview (the GitHub Settings → Security → Code quality
55
page) is only available to Team/Enterprise Cloud org plans, so it cannot be
66
enabled on this repo's free org. This script provides an equivalent gate by
7-
running the ``python-code-quality.qls`` suite via the CodeQL CLI in CI and
8-
failing the job when any finding remains.
7+
running the ``<language>-code-quality.qls`` suite via the CodeQL CLI in CI
8+
and failing the job when any finding remains. The workflow also feeds the
9+
default ``<language>-code-scanning.qls`` security suite through the same
10+
gate, because GitHub default setup only analyzes master post-merge and does
11+
not block PRs (see the header of codeql-quality.yml).
912
1013
Usage:
1114
codeql_quality_gate.py <quality.sarif>
@@ -160,6 +163,171 @@
160163
"heuristic to have correctly caught. mypy already confirms this file is "
161164
"clean under that typing.",
162165
),
166+
# ---- Security-suite entries (the workflow also gates the default
167+
# <language>-code-scanning suites; see codeql-quality.yml header). Entries
168+
# whose message substring is generic ("as clear text", "hashing algorithm")
169+
# are PATH-WIDE for that rule+file — a future finding of the same rule in
170+
# the same file would be suppressed too. Accepted for the same reason as
171+
# the quality entries above: the files are small and each reason names the
172+
# exact intended pattern. Re-audit when one of these files grows.
173+
(
174+
"py/clear-text-logging-sensitive-data",
175+
"custom_components/ha_mcp_tools/embedded_setup.py",
176+
"as clear text",
177+
"Deliberate admin-only connect instructions: the startup log prints the "
178+
"legacy-OAuth Client ID/Secret so the admin can paste them into an MCP "
179+
"client. The SECURITY note from the #1880 review in this file governs "
180+
"the pattern: credentials are withheld while a rotation is pending and "
181+
"never placed in the persistent notification all users can see.",
182+
),
183+
(
184+
"py/clear-text-logging-sensitive-data",
185+
"homeassistant-addon-webhook-proxy/mcp_proxy/__init__.py",
186+
"as clear text",
187+
"False positive: the log line emits oauth_provider.client_id_masked(), "
188+
"not the raw value; CodeQL tracks taint through the masking helper.",
189+
),
190+
(
191+
"py/clear-text-logging-sensitive-data",
192+
"homeassistant-addon-webhook-proxy-dev/mcp_proxy_dev/__init__.py",
193+
"as clear text",
194+
"False positive (dev flavor, identical code to stable): the log line "
195+
"emits client_id_masked(), not the raw value; CodeQL tracks taint "
196+
"through the masking helper.",
197+
),
198+
(
199+
"py/clear-text-logging-sensitive-data",
200+
"homeassistant-addon-webhook-proxy/start.py",
201+
"as clear text",
202+
"Deliberate add-on startup log: prints the connect URL and legacy-OAuth "
203+
"credentials to the Supervisor add-on log (admin-only) as the user's "
204+
"setup instructions — the add-on-side mirror of embedded_setup.py's "
205+
"admin-only connect log.",
206+
),
207+
(
208+
"py/clear-text-logging-sensitive-data",
209+
"homeassistant-addon-webhook-proxy-dev/start.py",
210+
"as clear text",
211+
"Deliberate add-on startup log (dev flavor, identical code to stable): "
212+
"prints the connect URL and legacy-OAuth credentials to the Supervisor "
213+
"add-on log (admin-only) as the user's setup instructions.",
214+
),
215+
(
216+
"py/clear-text-logging-sensitive-data",
217+
"src/ha_mcp/stdio_settings_sidecar.py",
218+
"as clear text",
219+
"Deliberate: logs the sidecar's own loopback settings URL "
220+
"(http://127.0.0.1:<port><secret_path>/settings) so the local operator "
221+
"can open it. Local-process log/stderr only; the URL is unreachable off "
222+
"the host.",
223+
),
224+
(
225+
"py/clear-text-logging-sensitive-data",
226+
"tests/test_env_manager.py",
227+
"as clear text",
228+
"Interactive test-environment helper printing the seeded throwaway "
229+
"credentials of the disposable HA test container (tests/test_constants.py). "
230+
"Printing them for copy-paste is the tool's purpose.",
231+
),
232+
(
233+
"py/clear-text-storage-sensitive-data",
234+
"homeassistant-addon-webhook-proxy/mcp_proxy/oauth.py",
235+
"as clear text",
236+
"Warned fallback: the primary path writes the signing key via "
237+
"_atomic_write_0600; the flagged plain write only runs when the "
238+
"filesystem cannot honor 0600 and it logs a warning. Persisting the key "
239+
"is the feature (it must survive restarts).",
240+
),
241+
(
242+
"py/clear-text-storage-sensitive-data",
243+
"homeassistant-addon-webhook-proxy-dev/mcp_proxy_dev/oauth.py",
244+
"as clear text",
245+
"Warned fallback (dev flavor, identical code to stable): plain write of "
246+
"the signing key only when the filesystem cannot honor 0600, with a "
247+
"warning. Persistence is the feature.",
248+
),
249+
(
250+
"py/clear-text-storage-sensitive-data",
251+
"homeassistant-addon-webhook-proxy/start.py",
252+
"as clear text",
253+
"Stable flavor pending promote: the creds file uses _atomic_write_0600 "
254+
"with a warned plain-write fallback; the proxy-config handoff write "
255+
"gained the same 0600-first treatment on the dev flavor (v2.0.3.dev1) "
256+
"and reaches this tree via the promote workflow — the stable-guard "
257+
"blocks editing it directly here. Remove the plain-write half of this "
258+
"reason after the next promote.",
259+
),
260+
(
261+
"py/clear-text-storage-sensitive-data",
262+
"homeassistant-addon-webhook-proxy-dev/start.py",
263+
"as clear text",
264+
"Warned fallbacks (dev flavor): both the creds file and the "
265+
"proxy-config handoff file write via _atomic_write_0600; the flagged "
266+
"plain writes only run when the filesystem cannot honor 0600 and each "
267+
"logs a warning. Persistence is the feature.",
268+
),
269+
(
270+
"py/weak-sensitive-data-hashing",
271+
"custom_components/ha_mcp_tools/oauth_legacy.py",
272+
"hashing algorithm (SHA256)",
273+
"Not password storage: SHA256 builds a change-detection fingerprint of "
274+
"the OAuth identity bound to the root views (machine-generated client "
275+
"secret + 256-bit signing key) to decide when routes must be rebound. "
276+
"KDFs exist to slow guessing of low-entropy human passwords; a "
277+
"fingerprint of high-entropy random material has no guessing surface.",
278+
),
279+
(
280+
"py/weak-sensitive-data-hashing",
281+
"homeassistant-addon-webhook-proxy/mcp_proxy/__init__.py",
282+
"hashing algorithm (SHA256)",
283+
"Not password storage: same _oauth_route_fingerprint helper as "
284+
"oauth_legacy.py — a SHA256 change-detection fingerprint of "
285+
"machine-generated high-entropy credentials, not a stored password hash.",
286+
),
287+
(
288+
"py/weak-sensitive-data-hashing",
289+
"homeassistant-addon-webhook-proxy-dev/mcp_proxy_dev/__init__.py",
290+
"hashing algorithm (SHA256)",
291+
"Not password storage (dev flavor, identical code to stable): SHA256 "
292+
"change-detection fingerprint of machine-generated high-entropy "
293+
"credentials, not a stored password hash.",
294+
),
295+
(
296+
"py/bad-tag-filter",
297+
"tests/src/unit/_js_harness.py",
298+
"does not match upper case",
299+
"Not a sanitizer: the JSDOM harness extracts <script> bodies from "
300+
"repo-authored, lowercase templates for parse/behaviour testing; "
301+
"untrusted HTML never flows through it.",
302+
),
303+
(
304+
"py/incomplete-url-substring-sanitization",
305+
"tests/src/unit/test_best_practice_checker.py",
306+
"may be at an arbitrary position",
307+
"Test assertion, not URL validation: checks that a warning message "
308+
"mentions the configured skill-prefix host.",
309+
),
310+
(
311+
"py/incomplete-url-substring-sanitization",
312+
"tests/src/unit/test_browser_landing.py",
313+
"may be at an arbitrary position",
314+
"Test assertion, not URL validation: checks that the landing page's "
315+
"help copy mentions dash.cloudflare.com.",
316+
),
317+
(
318+
"py/incomplete-url-substring-sanitization",
319+
"tests/src/unit/test_oauth.py",
320+
"may be at an arbitrary position",
321+
"Test assertion, not URL validation: checks that the consent HTML "
322+
"displays the redirect host to the user.",
323+
),
324+
(
325+
"py/incomplete-url-substring-sanitization",
326+
"tests/src/unit/test_oauth_legacy_component.py",
327+
"may be at an arbitrary position",
328+
"Test assertion, not URL validation: the XSS-escape test checks the "
329+
"redirect host appears (escaped) in the response body.",
330+
),
163331
)
164332

165333

0 commit comments

Comments
 (0)