Compare commits

...

8 Commits

Author SHA1 Message Date
dgtlmoon
fecd181e07 oops
Some checks failed
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
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
2026-03-02 11:15:12 +01:00
dgtlmoon
525e390523 test env tweaks 2026-03-02 11:10:28 +01:00
dgtlmoon
7fe332ad95 Small fix for 3.14 setup
Some checks failed
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (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 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 / 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
2026-03-02 10:37:02 +01:00
dgtlmoon
b65a01ec02 Python 3.14 test #3662 2026-03-02 10:37:02 +01:00
dgtlmoon
b984426666 0.54.3
Some checks failed
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 App Test / lint-code (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
2026-03-01 00:18:45 +01:00
dgtlmoon
1889a10ef6 CVE-2026-27696 Small fix - Restricted hostnames can still be added but are only checked at fetch-time (not when rendering lists etc) (#3938) 2026-03-01 00:17:29 +01:00
dgtlmoon
f66ae4fceb Adding Ukranian translations, rebuilding translations. (#3936) 2026-02-28 21:59:44 +01:00
Rithy-Nicolas TAN
fb14229888 Update messages.po in French translation (#3926) 2026-02-28 21:20:20 +01:00
19 changed files with 3110 additions and 32 deletions

View File

@@ -52,4 +52,13 @@ jobs:
uses: ./.github/workflows/test-stack-reusable-workflow.yml uses: ./.github/workflows/test-stack-reusable-workflow.yml
with: with:
python-version: '3.13' python-version: '3.13'
skip-pypuppeteer: true skip-pypuppeteer: true
test-application-3-14:
#if: github.event_name == 'push' && github.ref == 'refs/heads/master'
needs: lint-code
uses: ./.github/workflows/test-stack-reusable-workflow.yml
with:
python-version: '3.14'
skip-pypuppeteer: false

View File

@@ -706,7 +706,19 @@ jobs:
- name: Check upgrade works without error - name: Check upgrade works without error
run: | run: |
echo "=== Testing upgrade path from 0.49.1 to ${{ github.ref_name }} (${{ github.sha }}) ===" echo "=== Testing upgrade path from 0.49.1 to ${{ github.ref_name }} (${{ github.sha }}) ==="
sudo apt-get update && sudo apt-get install -y --no-install-recommends \
g++ \
gcc \
libc-dev \
libffi-dev \
libjpeg-dev \
libssl-dev \
libxslt-dev \
make \
patch \
pkg-config \
zlib1g-dev
# Checkout old version and create datastore # Checkout old version and create datastore
git checkout 0.49.1 git checkout 0.49.1
python3 -m venv .venv python3 -m venv .venv

View File

@@ -2,7 +2,7 @@
# Read more https://github.com/dgtlmoon/changedetection.io/wiki # Read more https://github.com/dgtlmoon/changedetection.io/wiki
# Semver means never use .01, or 00. Should be .1. # Semver means never use .01, or 00. Should be .1.
__version__ = '0.54.2' __version__ = '0.54.3'
from changedetectionio.strtobool import strtobool from changedetectionio.strtobool import strtobool
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError

View File

@@ -37,6 +37,7 @@ def get_timeago_locale(flask_locale):
'no': 'nb_NO', # Norwegian Bokmål 'no': 'nb_NO', # Norwegian Bokmål
'hi': 'in_HI', # Hindi 'hi': 'in_HI', # Hindi
'cs': 'en', # Czech not supported by timeago, fallback to English 'cs': 'en', # Czech not supported by timeago, fallback to English
'uk': 'uk', # Ukrainian
'en_GB': 'en', # British English - timeago uses 'en' 'en_GB': 'en', # British English - timeago uses 'en'
'en_US': 'en', # American English - timeago uses 'en' 'en_US': 'en', # American English - timeago uses 'en'
} }
@@ -67,6 +68,7 @@ LANGUAGE_DATA = {
'tr': {'flag': 'fi fi-tr fis', 'name': 'Türkçe'}, 'tr': {'flag': 'fi fi-tr fis', 'name': 'Türkçe'},
'ar': {'flag': 'fi fi-sa fis', 'name': 'العربية'}, 'ar': {'flag': 'fi fi-sa fis', 'name': 'العربية'},
'hi': {'flag': 'fi fi-in fis', 'name': 'हिन्दी'}, 'hi': {'flag': 'fi fi-in fis', 'name': 'हिन्दी'},
'uk': {'flag': 'fi fi-ua fis', 'name': 'Українська'},
} }

View File

@@ -2,6 +2,7 @@
import psutil import psutil
import time import time
from threading import Thread from threading import Thread
import multiprocessing
import pytest import pytest
import arrow import arrow
@@ -191,6 +192,34 @@ def cleanup(datastore_path):
if os.path.isfile(f): if os.path.isfile(f):
os.unlink(f) os.unlink(f)
def pytest_configure(config):
"""Configure pytest environment before tests run.
CRITICAL: Set multiprocessing start method to 'fork' for Python 3.14+ compatibility.
Python 3.14 changed the default start method from 'fork' to 'forkserver' on Linux.
The forkserver method requires all objects to be picklable, but pytest-flask's
LiveServer uses nested functions that can't be pickled.
Setting 'fork' explicitly:
- Maintains compatibility with Python 3.10-3.13 (where 'fork' was already default)
- Fixes Python 3.14 pickling errors
- Only affects Unix-like systems (Windows uses 'spawn' regardless)
See: https://github.com/python/cpython/issues/126831
See: https://docs.python.org/3/whatsnew/3.14.html
"""
# Only set if not already set (respects existing configuration)
if multiprocessing.get_start_method(allow_none=True) is None:
try:
# 'fork' is available on Unix-like systems (Linux, macOS)
# On Windows, this will have no effect as 'spawn' is the only option
multiprocessing.set_start_method('fork', force=False)
logger.debug("Set multiprocessing start method to 'fork' for Python 3.14+ compatibility")
except (ValueError, RuntimeError):
# Already set, not available on this platform, or context already created
pass
def pytest_addoption(parser): def pytest_addoption(parser):
"""Add custom command-line options for pytest. """Add custom command-line options for pytest.

View File

@@ -584,13 +584,16 @@ def test_static_directory_traversal(client, live_server, measure_memory_usage, d
def test_ssrf_private_ip_blocked(client, live_server, monkeypatch, measure_memory_usage, datastore_path): def test_ssrf_private_ip_blocked(client, live_server, monkeypatch, measure_memory_usage, datastore_path):
""" """
SSRF protection: IANA-reserved/private IP addresses must be blocked by default. SSRF protection: IANA-reserved/private IP addresses are blocked at fetch-time, not add-time.
Watches targeting private/reserved IPs can be *added* freely; the block happens when the
fetcher actually tries to reach the URL (via validate_iana_url() in call_browser()).
Covers: Covers:
1. is_private_hostname() correctly classifies all reserved ranges 1. is_private_hostname() correctly classifies all reserved ranges
2. is_safe_valid_url() rejects private-IP URLs at add-time (env var off) 2. is_safe_valid_url() ALLOWS private-IP URLs at add-time (IANA check moved to fetch-time)
3. is_safe_valid_url() allows private-IP URLs when ALLOW_IANA_RESTRICTED_ADDRESSES=true 3. ALLOW_IANA_RESTRICTED_ADDRESSES has no effect on add-time; it only controls fetch-time
4. UI form rejects private-IP URLs and shows the standard error message 4. UI form accepts private-IP URLs at add-time without error
5. Requests fetcher blocks fetch-time DNS rebinding (fresh check on every fetch) 5. Requests fetcher blocks fetch-time DNS rebinding (fresh check on every fetch)
6. Requests fetcher blocks redirects that lead to a private IP (open-redirect bypass) 6. Requests fetcher blocks redirects that lead to a private IP (open-redirect bypass)
@@ -623,9 +626,10 @@ def test_ssrf_private_ip_blocked(client, live_server, monkeypatch, measure_memor
assert not is_private_hostname(host), f"{host} should be identified as public" assert not is_private_hostname(host), f"{host} should be identified as public"
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# 2. is_safe_valid_url() blocks private-IP URLs (env var off) # 2. is_safe_valid_url() ALLOWS private-IP URLs at add-time
# IANA check is no longer done here — it moved to fetch-time validate_iana_url()
# ------------------------------------------------------------------ # ------------------------------------------------------------------
blocked_urls = [ private_ip_urls = [
'http://127.0.0.1/', 'http://127.0.0.1/',
'http://10.0.0.1/', 'http://10.0.0.1/',
'http://172.16.0.1/', 'http://172.16.0.1/',
@@ -636,21 +640,24 @@ def test_ssrf_private_ip_blocked(client, live_server, monkeypatch, measure_memor
'http://[fc00::1]/', 'http://[fc00::1]/',
'http://[fe80::1]/', 'http://[fe80::1]/',
] ]
for url in blocked_urls: for url in private_ip_urls:
assert not is_safe_valid_url(url), f"{url} should be blocked by is_safe_valid_url" assert is_safe_valid_url(url), f"{url} should be allowed by is_safe_valid_url (IANA check is at fetch-time)"
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# 3. ALLOW_IANA_RESTRICTED_ADDRESSES=true bypasses the block # 3. ALLOW_IANA_RESTRICTED_ADDRESSES does not affect add-time validation
# It only controls fetch-time blocking inside validate_iana_url()
# ------------------------------------------------------------------ # ------------------------------------------------------------------
monkeypatch.setenv('ALLOW_IANA_RESTRICTED_ADDRESSES', 'true') monkeypatch.setenv('ALLOW_IANA_RESTRICTED_ADDRESSES', 'true')
assert is_safe_valid_url('http://127.0.0.1/'), \ assert is_safe_valid_url('http://127.0.0.1/'), \
"Private IP should be allowed when ALLOW_IANA_RESTRICTED_ADDRESSES=true" "Private IP should be allowed at add-time regardless of ALLOW_IANA_RESTRICTED_ADDRESSES"
# Restore the block for the remaining assertions
monkeypatch.setenv('ALLOW_IANA_RESTRICTED_ADDRESSES', 'false') monkeypatch.setenv('ALLOW_IANA_RESTRICTED_ADDRESSES', 'false')
assert is_safe_valid_url('http://127.0.0.1/'), \
"Private IP should be allowed at add-time regardless of ALLOW_IANA_RESTRICTED_ADDRESSES"
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# 4. UI form rejects private-IP URLs # 4. UI form accepts private-IP URLs at add-time
# The watch is created; the SSRF block fires later at fetch-time
# ------------------------------------------------------------------ # ------------------------------------------------------------------
for url in ['http://127.0.0.1/', 'http://169.254.169.254/latest/meta-data/']: for url in ['http://127.0.0.1/', 'http://169.254.169.254/latest/meta-data/']:
res = client.post( res = client.post(
@@ -658,8 +665,8 @@ def test_ssrf_private_ip_blocked(client, live_server, monkeypatch, measure_memor
data={'url': url, 'tags': ''}, data={'url': url, 'tags': ''},
follow_redirects=True follow_redirects=True
) )
assert b'Watch protocol is not permitted or invalid URL format' in res.data, \ assert b'Watch protocol is not permitted or invalid URL format' not in res.data, \
f"UI should reject {url}" f"UI should accept {url} at add-time (SSRF is blocked at fetch-time)"
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# 5. Fetch-time DNS-rebinding check in the requests fetcher # 5. Fetch-time DNS-rebinding check in the requests fetcher

View File

@@ -1978,7 +1978,7 @@ msgstr "Format d'heure invalide. Utilisez HH:MM."
#: changedetectionio/forms.py #: changedetectionio/forms.py
msgid "Not a valid timezone name" msgid "Not a valid timezone name"
msgstr "Ce n'est pas un nom de fuseau horaire valide" msgstr "Nom de fuseau horaire invalide"
#: changedetectionio/forms.py #: changedetectionio/forms.py
msgid "not set" msgid "not set"
@@ -2054,9 +2054,7 @@ msgstr "secondes"
#: changedetectionio/forms.py #: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used" msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "" msgstr "Le corps et le titre de la notification sont requis lorsqu'une URL de notification est utilisée"
"Le corps et le titre de la notification sont requis lorsqu'une URL de notification est utiliséeLe corps et le titre "
"de la notification sont requis lorsqu'une URL de notification est utilisée"
#: changedetectionio/forms.py #: changedetectionio/forms.py
#, python-format #, python-format
@@ -2185,11 +2183,11 @@ msgstr "Utilisez les paramètres globaux pour le temps entre la vérification et
#: changedetectionio/forms.py #: changedetectionio/forms.py
msgid "CSS/JSONPath/JQ/XPath Filters" msgid "CSS/JSONPath/JQ/XPath Filters"
msgstr "Filtre CSS/xPath" msgstr "Filtre CSS/JSONPath/JQ/XPath"
#: changedetectionio/forms.py #: changedetectionio/forms.py
msgid "Remove elements" msgid "Remove elements"
msgstr "Sélectionner par élément" msgstr "Supprimer par élément"
#: changedetectionio/forms.py #: changedetectionio/forms.py
msgid "Extract text" msgid "Extract text"
@@ -2337,7 +2335,7 @@ msgstr "URL du proxy"
#: changedetectionio/forms.py #: changedetectionio/forms.py
msgid "Proxy URLs must start with http://, https:// or socks5://" msgid "Proxy URLs must start with http://, https:// or socks5://"
msgstr "Les URL proxy doivent commencer par http://, https:// ou chaussettes5://" msgstr "Les URL proxy doivent commencer par http://, https:// ou socks5://"
#: changedetectionio/forms.py #: changedetectionio/forms.py
msgid "Browser connection URL" msgid "Browser connection URL"

File diff suppressed because it is too large Load Diff

View File

@@ -71,12 +71,15 @@ def is_private_hostname(hostname):
for info in socket.getaddrinfo(hostname, None): for info in socket.getaddrinfo(hostname, None):
ip = ipaddress.ip_address(info[4][0]) ip = ipaddress.ip_address(info[4][0])
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved: if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
logger.warning(f"Hostname '{hostname} - {ip} - ip.is_private = {ip.is_private}, ip.is_loopback = {ip.is_loopback}, ip.is_link_local = {ip.is_link_local}, ip.is_reserved = {ip.is_reserved}")
return True return True
except socket.gaierror as e: except socket.gaierror as e:
logger.warning(f"{hostname} error checking {str(e)}") logger.warning(f"{hostname} error checking {str(e)}")
return False return False
logger.info(f"Hostname '{hostname}' is NOT private/IANA restricted.")
return False return False
def is_safe_valid_url(test_url): def is_safe_valid_url(test_url):
from changedetectionio import strtobool from changedetectionio import strtobool
from changedetectionio.jinja2_custom import render as jinja_render from changedetectionio.jinja2_custom import render as jinja_render
@@ -139,12 +142,4 @@ def is_safe_valid_url(test_url):
logger.warning(f'URL f"{test_url}" failed validation, aborting.') logger.warning(f'URL f"{test_url}" failed validation, aborting.')
return False return False
# Block IANA-restricted (private/reserved) IP addresses unless explicitly allowed.
# This is an add-time check; fetch-time re-validation in requests.py handles DNS rebinding.
if not strtobool(os.getenv('ALLOW_IANA_RESTRICTED_ADDRESSES', 'false')):
parsed = urlparse(test_url)
if parsed.hostname and is_private_hostname(parsed.hostname):
logger.warning(f'URL "{test_url}" resolves to a private/reserved IP address, aborting.')
return False
return True return True