Files
dgtlmoon e9e8c8d218
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/amd64 (alpine) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm64 (alpine) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/amd64 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm/v7 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm/v8 (main) (push) Has been cancelled
ChangeDetection.io Container Build Test / Build linux/arm64 (main) (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / lint-translations (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-14 (push) Has been cancelled
LLM / AI Change detection rules and Summaries
2026-04-24 15:21:09 +02:00

95 lines
3.1 KiB
Python

#!/usr/bin/env python3
# run from dir above changedetectionio/ dir
# python3 -m unittest changedetectionio.tests.unit.test_jq_security
import unittest
class TestJqExpressionSecurity(unittest.TestCase):
def test_blocked_builtins_raise(self):
"""Each dangerous builtin must be rejected by validate_jq_expression."""
from changedetectionio.html_tools import validate_jq_expression
blocked = [
# env access
'env',
'.foo | env',
'$ENV',
'$ENV.SECRET',
# file read via module system
'include "foo"',
'import "foo" as f',
# stdin reads
'input',
'inputs',
'[.,inputs]',
# process termination
'halt',
'halt_error(1)',
# stderr/debug leakage
'debug',
'. | debug | .foo',
'stderr',
# misc info leakage
'$__loc__',
'builtins',
'modulemeta',
'$JQ_BUILD_CONFIGURATION',
]
for expr in blocked:
with self.assertRaises(ValueError, msg=f"Expected ValueError for: {expr!r}"):
validate_jq_expression(expr)
def test_safe_expressions_pass(self):
"""Normal jq expressions must not be blocked."""
from changedetectionio.html_tools import validate_jq_expression
safe = [
'.foo',
'.items[] | .price',
'map(select(.active)) | length',
'.[] | select(.name | test("foo"))',
'to_entries | map(.value) | add',
'[.[] | .id] | unique',
'.price | tonumber',
'if .stock > 0 then "in stock" else "out of stock" end',
]
for expr in safe:
try:
validate_jq_expression(expr)
except ValueError as e:
self.fail(f"validate_jq_expression raised ValueError for safe expression {expr!r}: {e}")
def test_allow_risky_env_var_bypasses_check(self):
"""JQ_ALLOW_RISKY_EXPRESSIONS=true must skip all blocking."""
import os
from unittest.mock import patch
from changedetectionio.html_tools import validate_jq_expression
with patch.dict(os.environ, {'JQ_ALLOW_RISKY_EXPRESSIONS': 'true'}):
# Should not raise even for the most dangerous expression
try:
validate_jq_expression('env')
validate_jq_expression('$ENV')
except ValueError as e:
self.fail(f"Should not block when JQ_ALLOW_RISKY_EXPRESSIONS=true: {e}")
def test_allow_risky_env_var_off_by_default(self):
"""Without JQ_ALLOW_RISKY_EXPRESSIONS set, blocking must be active."""
import os
from unittest.mock import patch
from changedetectionio.html_tools import validate_jq_expression
env = {k: v for k, v in os.environ.items() if k != 'JQ_ALLOW_RISKY_EXPRESSIONS'}
with patch.dict(os.environ, env, clear=True):
with self.assertRaises(ValueError):
validate_jq_expression('env')
if __name__ == '__main__':
unittest.main()