CVE-2026-33981
Description
changedetection.io is a free open source web page change detection tool. Prior to 0.54.7, the jq: and jqraw: include filter expressions allow use of the jq env builtin, which reads all process environment variables and stores them as the watch snapshot. An authenticated user (or unauthenticated user when no password is set, the default) can leak sensitive environment variables including SALTED_PASS, PLAYWRIGHT_DRIVER_URL, HTTP_PROXY, and any secrets passed as env vars to the container. Version 0.54.7 patches the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
changedetection.ioPyPI | < 0.54.7 | 0.54.7 |
Affected products
1- cpe:2.3:a:webtechnologies:changedetection:*:*:*:*:*:*:*:*Range: <0.54.7
Patches
165517a9c74a0CVE-2026-33981 - Environment Variable Disclosure via jq env Builtin in Include Filters
2 files changed · +48 −2
changedetectionio/forms.py+2 −0 modified@@ -667,9 +667,11 @@ def __call__(self, form, field): # `jq` requires full compilation in windows and so isn't generally available raise ValidationError("jq not support not found") + from changedetectionio.html_tools import validate_jq_expression input = line.replace('jq:', '') try: + validate_jq_expression(input) jq.compile(input) except (ValueError) as e: message = field.gettext('\'%s\' is not a valid jq expression. (%s)')
changedetectionio/html_tools.py+46 −2 modified@@ -4,6 +4,7 @@ from typing import List import html import json +import os import re # HTML added to be sure each result matching a filter (.example) gets converted to a new line by Inscriptis @@ -13,6 +14,45 @@ TITLE_RE = re.compile(r"<title[^>]*>(.*?)</title>", re.I | re.S) META_CS = re.compile(r'<meta[^>]+charset=["\']?\s*([a-z0-9_\-:+.]+)', re.I) + +# jq builtins that can leak sensitive data or cause harm when user-supplied expressions are executed. +# env/$ENV reads all process environment variables (passwords, API keys, etc.) +# include/import can read arbitrary files from disk +# input/inputs reads beyond the supplied JSON data +# debug/stderr leaks data to stderr +# halt/halt_error terminates the process (DoS) +_JQ_BLOCKED_PATTERNS = [ + (re.compile(r'\benv\b'), 'env (reads environment variables)'), + (re.compile(r'\$ENV\b'), '$ENV (reads environment variables)'), + (re.compile(r'\binclude\b'), 'include (reads files from disk)'), + (re.compile(r'\bimport\b'), 'import (reads files from disk)'), + (re.compile(r'\binputs?\b'), 'input/inputs (reads beyond provided data)'), + (re.compile(r'\bdebug\b'), 'debug (leaks data to stderr)'), + (re.compile(r'\bstderr\b'), 'stderr (leaks data to stderr)'), + (re.compile(r'\bhalt(?:_error)?\b'), 'halt/halt_error (terminates the process)'), + (re.compile(r'\$__loc__\b'), '$__loc__ (leaks file path information)'), + (re.compile(r'\bbuiltins\b'), 'builtins (enumerates available functions)'), + (re.compile(r'\bmodulemeta\b'), 'modulemeta (leaks module information)'), + (re.compile(r'\$JQ_BUILD_CONFIGURATION\b'), '$JQ_BUILD_CONFIGURATION (leaks build information)'), +] + +def validate_jq_expression(expression: str) -> None: + """Raise ValueError if the jq expression uses any dangerous builtin. + + User-supplied jq expressions are executed server-side. Without this check, + builtins like `env` expose every process environment variable (SALTED_PASS, + proxy credentials, API keys, etc.) as watch output. + """ + from changedetectionio.strtobool import strtobool + if strtobool(os.getenv('JQ_ALLOW_RISKY_EXPRESSIONS', 'false')): + return + + for pattern, description in _JQ_BLOCKED_PATTERNS: + if pattern.search(expression): + msg = f"jq expression uses disallowed builtin: {description}" + logger.critical(f"Security: blocked jq expression containing '{description}' - expression: {expression!r}") + raise ValueError(msg) + META_CT = re.compile(r'<meta[^>]+http-equiv=["\']?content-type["\']?[^>]*content=["\'][^>]*charset=([a-z0-9_\-:+.]+)', re.I) # 'price' , 'lowPrice', 'highPrice' are usually under here @@ -378,12 +418,16 @@ def _parse_json(json_data, json_filter): raise Exception("jq not support not found") if json_filter.startswith("jq:"): - jq_expression = jq.compile(json_filter.removeprefix("jq:")) + expr = json_filter.removeprefix("jq:") + validate_jq_expression(expr) + jq_expression = jq.compile(expr) match = jq_expression.input(json_data).all() return _get_stripped_text_from_json_match(match) if json_filter.startswith("jqraw:"): - jq_expression = jq.compile(json_filter.removeprefix("jqraw:")) + expr = json_filter.removeprefix("jqraw:") + validate_jq_expression(expr) + jq_expression = jq.compile(expr) match = jq_expression.input(json_data).all() return '\n'.join(str(item) for item in match)
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/dgtlmoon/changedetection.io/commit/65517a9c74a0cbe1a4661314470b28131ef5557fnvdPatchWEB
- github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-58r7-4wr5-hfx8nvdExploitVendor AdvisoryWEB
- github.com/advisories/GHSA-58r7-4wr5-hfx8ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-33981ghsaADVISORY
- github.com/dgtlmoon/changedetection.io/releases/tag/0.54.7nvdProductRelease NotesWEB
News mentions
0No linked articles in our index yet.