specter-desktop/tests/test_helpers.py
al-munazzim 041df50d0d
Fix open redirect bypass via percent-encoded characters (issue #2590) (#2596)
Co-authored-by: Nazim <nazim@openclaw.ai>
Co-authored-by: k9ert <117085+k9ert@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:17:11 +02:00

159 lines
5.3 KiB
Python

import logging
from cryptoadvance.specter.helpers import (
deep_update,
load_jsons,
is_ip_private,
create_unique_id,
)
logger = logging.getLogger(__name__)
def test_deep_update():
base_value = {
"pli": {"pla": "blub", "yes": "yes"},
"pli2": ["arrayelm1", "arrayelm2", "arrayelm3"],
"pli3": "aStringValue",
}
assert len(base_value) == 3
assert len(base_value["pli"]) == 2
assert len(base_value["pli2"]) == 3
update_value = {
"newRootKey": {"pla": "blub"},
"pli": {"newSubKey": "blub2"},
"pli2": ["arrayelm4", "arrayelm5"],
}
deep_update(base_value, update_value)
# There is now a newRootKey
assert len(base_value) == 4
# keys get added
assert len(base_value["pli"]) == 3
# Arrays get replaced, not appended!
assert len(base_value["pli2"]) == 2
# you cannot delete stuff with empty dicts
deep_update(base_value, {"newRootKey": {}})
assert base_value["newRootKey"]["pla"] == "blub"
def test_load_jsons(caplog):
caplog.set_level(logging.INFO)
caplog.set_level(logging.DEBUG, logger="cryptoadvance.specter")
mydict = load_jsons("./tests/helpers_testdata")
assert mydict["some_jsonfile"]["blub"] == "bla"
assert mydict["some_other_jsonfile"]["bla"] == "blub"
mydict = load_jsons("./tests/helpers_testdata", "id")
assert "some_jsonfile" not in mydict
# instead the value for the key "id" is now used as the top-level key
assert mydict["ID123"]["blub"] == "bla"
# This also assumes that the key is unique!!!!
assert mydict["ID124"]["bla"] == "blub"
# ToDo: check the uniqueness in the implementation to avoid issues
# the filename is added as alias
assert mydict["ID123"]["alias"] == "some_jsonfile"
# We also get the fullpath of that file:
assert mydict["ID123"]["fullpath"] == "./tests/helpers_testdata/some_jsonfile.json"
# Quite handy if you want to get rid of it which is as easy as:
# os.remove(mydict["ID123"]['fullpath'])
def test_is_ip_private(caplog):
caplog.set_level(logging.INFO)
caplog.set_level(logging.DEBUG, logger="cryptoadvance.specter")
# https://en.wikipedia.org/wiki/Private_network
# For each private network address range, we are testing the lower, arbitrary middle and upper part of the range
priv_lo_addresses = ["127.0.0.0", "127.1.32.15", "127.255.255.255"]
priv_24_addresses = ["10.0.0.0", "10.1.32.125", "10.255.255.255"]
priv_20_addresses = ["172.16.0.0", "172.24.32.125", "172.31.255.255"]
priv_16_addresses = ["192.168.0.0", "192.168.16.125", "192.168.255.255"]
# Randomly generated public ip addresses
public_addresses = ["58.28.76.138", "128.5.218.201", "170.45.214.32"]
assert is_ip_private("localhost")
for ip in priv_lo_addresses:
assert is_ip_private(ip)
for ip in priv_24_addresses:
assert is_ip_private(ip)
for ip in priv_20_addresses:
assert is_ip_private(ip)
for ip in priv_16_addresses:
assert is_ip_private(ip)
for ip in public_addresses:
assert not is_ip_private(ip)
def test_create_unique_id():
the_same_unique_names = [
"ghost wallet",
"Ghost Wallet",
"GHOST WALLET",
"gHoSt wALlEt",
" Ghost-Wallet",
"Ghost-Wallet ",
" Ghost-Wallet ",
"ghost-wallet",
"Ghost_Wallet",
"ghost_wallet",
"ghost wallet",
"ghost----wallet",
"Ghost Wallet?",
"Ghost Wallet***",
]
assert all(
create_unique_id(name) == "ghost_wallet" for name in the_same_unique_names
)
not_the_same_unique_names = [
"ghost wallet",
"ghost wallet 2",
"ghostwallet",
"ghost.wallet",
"ghostwallet123",
"my_ghos_wallet",
]
assert not all(
create_unique_id(name) == "ghost_wallet" for name in not_the_same_unique_names
)
def test_is_relative_url():
"""Test is_relative_url() for open redirect prevention."""
from cryptoadvance.specter.helpers import is_relative_url
# Valid relative URLs - should pass
assert is_relative_url("/")
assert is_relative_url("/valid/path")
assert is_relative_url("/path?query=value")
assert is_relative_url("/path#anchor")
assert is_relative_url("/a/b/c/d/e")
# Invalid - protocol-relative URLs (should be rejected)
assert not is_relative_url("//evil.com")
# Invalid - percent-encoded bypass attempts
assert not is_relative_url("/%09/baidu.com") # tab -> // bypass
assert not is_relative_url("/%09baidu.com")
assert not is_relative_url("/%5c%5cevil.com") # backslash encoding
assert not is_relative_url("/%20/path") # space
# Invalid - control characters
assert not is_relative_url("/\x00/path")
assert not is_relative_url("/\x1f/path")
assert not is_relative_url("/\x7f/path") # DEL character
# Invalid - whitespace
assert not is_relative_url("/ path")
assert not is_relative_url("/path ") # trailing
assert not is_relative_url(" /path") # leading
# Invalid - backslashes
assert not is_relative_url("/\\path")
assert not is_relative_url("/path\\to\\file")
# Invalid - non-relative
assert not is_relative_url("")
assert not is_relative_url("https://evil.com")
# /https:// passes validation - it's a local server path, not external
assert is_relative_url("/https://evil.com")