VYPR
Medium severity6.5NVD Advisory· Published Mar 27, 2026· Updated Apr 2, 2026

CVE-2026-33981

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.

PackageAffected versionsPatched versions
changedetection.ioPyPI
< 0.54.70.54.7

Affected products

1

Patches

1
65517a9c74a0

CVE-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

News mentions

0

No linked articles in our index yet.