Compare commits

...

4 Commits

Author SHA1 Message Date
dgtlmoon
b984426666 0.54.3
Some checks are pending
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
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
16 changed files with 3058 additions and 30 deletions

View File

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

View File

@@ -37,6 +37,7 @@ def get_timeago_locale(flask_locale):
'no': 'nb_NO', # Norwegian Bokmål
'hi': 'in_HI', # Hindi
'cs': 'en', # Czech not supported by timeago, fallback to English
'uk': 'uk', # Ukrainian
'en_GB': 'en', # British 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'},
'ar': {'flag': 'fi fi-sa fis', 'name': 'العربية'},
'hi': {'flag': 'fi fi-in fis', 'name': 'हिन्दी'},
'uk': {'flag': 'fi fi-ua fis', 'name': 'Українська'},
}

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):
"""
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:
1. is_private_hostname() correctly classifies all reserved ranges
2. is_safe_valid_url() rejects private-IP URLs at add-time (env var off)
3. is_safe_valid_url() allows private-IP URLs when ALLOW_IANA_RESTRICTED_ADDRESSES=true
4. UI form rejects private-IP URLs and shows the standard error message
2. is_safe_valid_url() ALLOWS private-IP URLs at add-time (IANA check moved to fetch-time)
3. ALLOW_IANA_RESTRICTED_ADDRESSES has no effect on add-time; it only controls fetch-time
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)
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"
# ------------------------------------------------------------------
# 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://10.0.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://[fe80::1]/',
]
for url in blocked_urls:
assert not is_safe_valid_url(url), f"{url} should be blocked by is_safe_valid_url"
for url in private_ip_urls:
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')
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')
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/']:
res = client.post(
@@ -658,8 +665,8 @@ def test_ssrf_private_ip_blocked(client, live_server, monkeypatch, measure_memor
data={'url': url, 'tags': ''},
follow_redirects=True
)
assert b'Watch protocol is not permitted or invalid URL format' in res.data, \
f"UI should reject {url}"
assert b'Watch protocol is not permitted or invalid URL format' not in res.data, \
f"UI should accept {url} at add-time (SSRF is blocked at fetch-time)"
# ------------------------------------------------------------------
# 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
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
msgid "not set"
@@ -2054,9 +2054,7 @@ msgstr "secondes"
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr ""
"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"
msgstr "Le corps et le titre de la notification sont requis lorsqu'une URL de notification est utilisée"
#: changedetectionio/forms.py
#, python-format
@@ -2185,11 +2183,11 @@ msgstr "Utilisez les paramètres globaux pour le temps entre la vérification et
#: changedetectionio/forms.py
msgid "CSS/JSONPath/JQ/XPath Filters"
msgstr "Filtre CSS/xPath"
msgstr "Filtre CSS/JSONPath/JQ/XPath"
#: changedetectionio/forms.py
msgid "Remove elements"
msgstr "Sélectionner par élément"
msgstr "Supprimer par élément"
#: changedetectionio/forms.py
msgid "Extract text"
@@ -2337,7 +2335,7 @@ msgstr "URL du proxy"
#: changedetectionio/forms.py
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
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):
ip = ipaddress.ip_address(info[4][0])
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
except socket.gaierror as e:
logger.warning(f"{hostname} error checking {str(e)}")
return False
logger.info(f"Hostname '{hostname}' is NOT private/IANA restricted.")
return False
def is_safe_valid_url(test_url):
from changedetectionio import strtobool
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.')
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