mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
feat: sanitize filenames in apt and yum modules to prevent path traversal
Signed-off-by: d9pouces <github@19pouces.net>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
3sxG
|
||||
=TrE4
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
@@ -0,0 +1,19 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBGqg6rMBCADaMUd27S41+kInPXuRZ0aL41aMZjqr+mw4W34w8i/JRYMJ6eGb
|
||||
sXf40AaFfS7zkbUpQZpmh3aYPF4jUAdWV03ZNNYdcGerIAKc2xsugSKjJIm1XPQq
|
||||
upz914b4/hp/UslDUH7SPZ+3Tq17TXW+xP6aUM9VcWMnlJT/DFgxh5sZB3nfDeYr
|
||||
yBV+PmEVfuQaUSIwKAh3Hv0ZaBHS8KzsawLzbJ+Pa9W9wjVME1izi9ctloGSxtLc
|
||||
p1suS2IS33QB0jGgj4VbuMcjo2iCpzW3Xd7jQ7WpXGjrl0bHnJTnWWB0ihDD3cJe
|
||||
tJlk9GScMQu3Z3th67DmqTXQru3btrDjxMifABEBAAG0NWlkLWNvbmZpZ3VyYXRv
|
||||
ci10ZXN0cyA8dGVzdHNAaWQtY29uZmlndXJhdG9yLmludmFsaWQ+iQFuBBMBCABY
|
||||
FiEEaZF4ALsB1xUU3IP5rl3Himib1RgFAmqg6rMbFIAAAAAABAAObWFudTIsMi41
|
||||
KzEuMTIsMCwzAxsvBAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRCuXceK
|
||||
aJvVGFRrB/wIbG1HHQANDCHgyl4TRTPuKBX2c9lmJi2plp+T5L5sXyTMbAMLfTCk
|
||||
JWL/zHLM3N2v6GZuzbwZM8Dzj9zLLMpC6newmZe4YX+ZtyZGufBI963kh8PvlSWW
|
||||
USyH+oYoIhlyJqj7tCQXoijEvQnSReKGHUTvQGvQK2Ybp735NmAiDOM3/3KKh2Dq
|
||||
as/Tc2mxec4whr8RyUidsF+Yp6wTesfMxeax6DmWIcSLDWpUBndeahZ2Ff0c96Bz
|
||||
bpF1v2hbvyI/IaP5snb4Nm1xpREm+y8iDAQhOklfXCyXrWgN3sIrk3tDndGrn+nw
|
||||
0iusnaBVki0keqjlH0camoUP28tu3sxG
|
||||
=qZrp
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
+22
-2
@@ -32,7 +32,7 @@ def make_config() -> Configuration:
|
||||
return Configuration.model_validate(SOURCE_CONTENT)
|
||||
|
||||
|
||||
def make_config_with(source_extra: dict) -> Configuration:
|
||||
def make_config_with(source_extra: dict, name: str = "interdiode") -> Configuration:
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
@@ -40,7 +40,7 @@ def make_config_with(source_extra: dict) -> Configuration:
|
||||
"apt": {
|
||||
"sources": [
|
||||
{
|
||||
"name": "interdiode",
|
||||
"name": name,
|
||||
"uris": ["http://example.invalid/debian"],
|
||||
"suites": ["stable"],
|
||||
**source_extra,
|
||||
@@ -227,6 +227,26 @@ def test_apply_without_signed_by_writes_no_keyring_or_field(
|
||||
)
|
||||
|
||||
|
||||
def test_apply_sanitizes_source_name_with_slash(tmp_path, monkeypatch) -> None:
|
||||
mock_run = _mock_gpg_dearmor_success(monkeypatch)
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(
|
||||
config=make_config_with({"signed-by": KEY_CONTENT}, name="../../evil"),
|
||||
dst=dst,
|
||||
src=None,
|
||||
)
|
||||
|
||||
AptModule().apply(context)
|
||||
|
||||
keyring = dst / "etc/apt/keyrings/.._.._evil.gpg"
|
||||
assert keyring.read_bytes() == KEYRING_BYTES
|
||||
source_file = dst / "etc/apt/sources.list.d/.._.._evil.sources"
|
||||
assert "Signed-By: /etc/apt/keyrings/.._.._evil.gpg" in source_file.read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
mock_run.assert_called_once()
|
||||
|
||||
|
||||
def test_resolve_signed_by_raises_when_gpg_not_on_path(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setattr("id_configurator.modules.apt.shutil.which", lambda _: None)
|
||||
dst = tmp_path / "dst"
|
||||
|
||||
@@ -54,6 +54,27 @@ def test_apply_writes_certificate_and_rebuilds_bundle(tmp_path) -> None:
|
||||
assert openssl_default.readlink().as_posix() == "/etc/ssl/certs/ca-certificates.crt"
|
||||
|
||||
|
||||
def test_apply_sanitizes_certificate_name_with_slash(tmp_path) -> None:
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"certificates": [{"name": "../../evil", "content": CERT_CONTENT}]
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=Configuration.model_validate(data), dst=dst, src=None)
|
||||
|
||||
CertificatesModule().apply(context)
|
||||
|
||||
local_cert = dst / "usr/local/share/ca-certificates/.._.._evil.crt"
|
||||
assert local_cert.read_bytes() == b"fake-cert-bytes"
|
||||
# The "/" must never be interpreted as a path separator: without
|
||||
# sanitizing, "../../evil" would have reshuffled the write up to
|
||||
# usr/local/evil.crt instead of staying under ca-certificates/.
|
||||
assert not (dst / "usr/local/evil.crt").exists()
|
||||
|
||||
|
||||
def test_apply_merges_conf_listed_and_local_certificates(tmp_path) -> None:
|
||||
src = tmp_path / "src"
|
||||
local_certs_dir = src / "usr/local/share/ca-certificates"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
from id_configurator.modules import base as base_module
|
||||
from id_configurator.modules.base import Module, all_modules, register
|
||||
from id_configurator.modules.base import (
|
||||
Module,
|
||||
all_modules,
|
||||
register,
|
||||
sanitize_filename,
|
||||
)
|
||||
|
||||
|
||||
def test_register_and_all_modules_are_isolated(monkeypatch) -> None:
|
||||
@@ -17,3 +22,15 @@ def test_register_and_all_modules_are_isolated(monkeypatch) -> None:
|
||||
register(DummyModule)
|
||||
|
||||
assert all_modules() == [DummyModule]
|
||||
|
||||
|
||||
def test_sanitize_filename_leaves_plain_names_untouched() -> None:
|
||||
assert sanitize_filename("interdiode") == "interdiode"
|
||||
|
||||
|
||||
def test_sanitize_filename_replaces_forward_slash() -> None:
|
||||
assert sanitize_filename("../../etc/passwd") == ".._.._etc_passwd"
|
||||
|
||||
|
||||
def test_sanitize_filename_replaces_backslash() -> None:
|
||||
assert sanitize_filename(r"a\b") == "a_b"
|
||||
|
||||
@@ -165,6 +165,36 @@ def test_apply_disables_existing_repos_before_adding_configured_ones(tmp_path) -
|
||||
)
|
||||
|
||||
|
||||
def test_apply_sanitizes_repository_and_key_names_with_slash(
|
||||
tmp_path, monkeypatch
|
||||
) -> None:
|
||||
mock_run = _mock_gpg_success(monkeypatch)
|
||||
data = {
|
||||
"uuid": "12345678-1234-5678-1234-567812345678",
|
||||
"organisation": "InterDiode",
|
||||
"configurations": {
|
||||
"yum": {
|
||||
"repositories": [
|
||||
{
|
||||
"name": "../../evil-repo",
|
||||
"index-url": "https://example.invalid/yum",
|
||||
}
|
||||
],
|
||||
"keys": [{"name": "../../evil-key", "content": KEY_CONTENT}],
|
||||
}
|
||||
},
|
||||
}
|
||||
dst = tmp_path / "dst"
|
||||
context = Context(config=make_config(data), dst=dst, src=None)
|
||||
|
||||
YumModule().apply(context)
|
||||
|
||||
assert (dst / "etc/yum.repos.d/.._.._evil-repo.repo").exists()
|
||||
key_content = (dst / "etc/pki/rpm-gpg/RPM-GPG-KEY-.._.._evil-key").read_bytes()
|
||||
assert key_content == b"fake-rpm-gpg-key-bytes"
|
||||
mock_run.assert_called_once()
|
||||
|
||||
|
||||
def test_import_key_raises_when_gpg_binary_missing(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"id_configurator.modules.yum.subprocess.run",
|
||||
|
||||
Reference in New Issue
Block a user