mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-10-23 19:02:18 +00:00
feat(tests): add property-based tests for hex colour parsing and repository upload filename sanitization to enhance robustness
This commit is contained in:
5 files changed
+164
No files matched your search
@@ -0,0 +1,46 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Property tests for hex colour parsing."""
|
||||
|
||||
import pytest
|
||||
from hypothesis import given, settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from meshchatx.src.backend.colour_utils import ColourUtils
|
||||
|
||||
|
||||
@settings(max_examples=200, deadline=None)
|
||||
@given(t=st.text(max_size=200))
|
||||
def test_hex_colour_only_value_error_or_returns_bytes(t):
|
||||
try:
|
||||
out = ColourUtils.hex_colour_to_byte_array(t)
|
||||
except ValueError:
|
||||
return
|
||||
except Exception as e:
|
||||
raise AssertionError(f"unexpected exception: {type(e).__name__}: {e}") from e
|
||||
|
||||
assert isinstance(out, bytes)
|
||||
|
||||
|
||||
@settings(max_examples=150, deadline=None)
|
||||
@given(
|
||||
h=st.from_regex(r"[0-9a-fA-F]{2,64}", fullmatch=True).filter(
|
||||
lambda x: len(x) % 2 == 0
|
||||
)
|
||||
)
|
||||
def test_hex_colour_even_length_hex_round_trip(h):
|
||||
out = ColourUtils.hex_colour_to_byte_array(h)
|
||||
out_hash = ColourUtils.hex_colour_to_byte_array("#" + h)
|
||||
assert out == out_hash
|
||||
assert len(out) == len(h) // 2
|
||||
|
||||
|
||||
@settings(max_examples=80, deadline=None)
|
||||
@given(
|
||||
h=st.from_regex(r"[0-9a-fA-F]{1,63}", fullmatch=True).filter(
|
||||
lambda x: len(x) % 2 == 1
|
||||
)
|
||||
)
|
||||
def test_hex_colour_odd_length_raises(h):
|
||||
with pytest.raises(ValueError):
|
||||
ColourUtils.hex_colour_to_byte_array(h)
|
||||
@@ -0,0 +1,60 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Property tests for repository upload filename sanitization."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from hypothesis import given, settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from meshchatx.src.backend.repository_server_manager import (
|
||||
RepositoryServerManager,
|
||||
_safe_any_upload_filename,
|
||||
)
|
||||
|
||||
_SAFE_RE = re.compile(r"[A-Za-z0-9._+\-]+")
|
||||
|
||||
|
||||
@settings(max_examples=250, deadline=None)
|
||||
@given(name=st.text(max_size=400))
|
||||
def test_safe_any_upload_filename_never_raises(name):
|
||||
out = _safe_any_upload_filename(name)
|
||||
if out is None:
|
||||
return
|
||||
assert isinstance(out, str)
|
||||
assert out == os.path.basename(name)
|
||||
assert out == name
|
||||
assert ".." not in out
|
||||
assert _SAFE_RE.fullmatch(out)
|
||||
|
||||
|
||||
@settings(max_examples=200, deadline=None)
|
||||
@given(
|
||||
base=st.from_regex(r"[A-Za-z0-9._+\-]{1,120}", fullmatch=True).filter(
|
||||
lambda b: b not in (".", "..") and ".." not in b
|
||||
),
|
||||
)
|
||||
def test_save_upload_roundtrip_for_generated_safe_names(base):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
mgr = RepositoryServerManager(td)
|
||||
ok, err = mgr.save_upload(base, b"\x00\x01\x02")
|
||||
assert ok and err is None
|
||||
dest = os.path.join(td, "repository-server", "uploads", base)
|
||||
assert dest == os.path.realpath(dest)
|
||||
assert os.path.isfile(dest)
|
||||
assert os.path.dirname(dest) == os.path.join(td, "repository-server", "uploads")
|
||||
|
||||
|
||||
@settings(max_examples=120, deadline=None)
|
||||
@given(
|
||||
prefix=st.text(max_size=80),
|
||||
base=st.from_regex(r"[A-Za-z0-9._+\-]{1,40}", fullmatch=True),
|
||||
)
|
||||
def test_save_upload_rejects_path_traversal_prefix(prefix, base):
|
||||
poison = prefix + "../" + base
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
mgr = RepositoryServerManager(td)
|
||||
ok, err = mgr.save_upload(poison, b"x")
|
||||
assert not ok
|
||||
@@ -41,4 +41,19 @@ describe("safeExternalUrl", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("fuzz: full BMP code units do not throw and only yield safe schemes", () => {
|
||||
for (let i = 0; i < 250; i++) {
|
||||
let s = "";
|
||||
const len = Math.floor(Math.random() * 400);
|
||||
for (let j = 0; j < len; j++) {
|
||||
s += String.fromCharCode(Math.floor(Math.random() * 65536));
|
||||
}
|
||||
expect(() => normalizeExternalUrlForOpen(s)).not.toThrow();
|
||||
const o = normalizeExternalUrlForOpen(s);
|
||||
if (o !== null) {
|
||||
expect(o.startsWith("http://") || o.startsWith("https://") || o.startsWith("mailto:")).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -57,4 +57,27 @@ describe("shellPathGuard", () => {
|
||||
fs.unlinkSync(p);
|
||||
}
|
||||
});
|
||||
|
||||
it("fuzz: paths under default storage stay allowed", () => {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const seg = Array.from({ length: 10 }, () => String.fromCharCode(Math.floor(Math.random() * 94) + 33)).join(
|
||||
""
|
||||
);
|
||||
const p = path.join(storage, "fuzz-shell", seg);
|
||||
expect(() => isAllowedShellPath(p, ctx)).not.toThrow();
|
||||
expect(isAllowedShellPath(p, ctx)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("fuzz: random absolute-looking paths return boolean without throw", () => {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
let s = process.platform === "win32" ? "C:\\" : "/";
|
||||
const n = Math.floor(Math.random() * 24) + 4;
|
||||
for (let j = 0; j < n; j++) {
|
||||
s += String.fromCharCode(Math.floor(Math.random() * 96) + 32);
|
||||
}
|
||||
expect(() => isAllowedShellPath(s, ctx)).not.toThrow();
|
||||
expect(typeof isAllowedShellPath(s, ctx)).toBe("boolean");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -178,4 +178,24 @@ describe("LinkUtils.js", () => {
|
||||
expect(LinkUtils.httpUrlHrefOrNull("file:///etc/passwd")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("fuzzing robustness", () => {
|
||||
it("httpUrlHrefOrNull and renderAllLinks tolerate random BMP strings", () => {
|
||||
for (let i = 0; i < 300; i++) {
|
||||
let s = "";
|
||||
const len = Math.floor(Math.random() * 600);
|
||||
for (let j = 0; j < len; j++) {
|
||||
s += String.fromCharCode(Math.floor(Math.random() * 65536));
|
||||
}
|
||||
expect(() => LinkUtils.httpUrlHrefOrNull(s)).not.toThrow();
|
||||
expect(() => LinkUtils.renderAllLinks(s)).not.toThrow();
|
||||
const h = LinkUtils.httpUrlHrefOrNull(s);
|
||||
if (h !== null) {
|
||||
expect(h.startsWith("http://") || h.startsWith("https://")).toBe(true);
|
||||
}
|
||||
const rendered = LinkUtils.renderAllLinks(s);
|
||||
expect(rendered.toLowerCase()).not.toContain("<script");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user