Better protection against symlink traversal in flatpak. Other hardening
This commit is contained in:
parent
903125976d
commit
f5b85d29d3
9 changed files with 429 additions and 53 deletions
|
|
@ -513,3 +513,68 @@ def test_flatpak_list_attempts_respect_supported_columns():
|
|||
assert any("--columns=application,branch" in cmd for cmd in command_strings)
|
||||
assert not any("origin" in cmd for cmd in command_strings)
|
||||
assert command_strings[-1] == "flatpak list --system"
|
||||
|
||||
|
||||
def test_user_flatpak_origin_symlink_is_not_read(tmp_path: Path):
|
||||
import enroll.accounts as a
|
||||
|
||||
home = tmp_path / "home"
|
||||
active = (
|
||||
home
|
||||
/ ".local"
|
||||
/ "share"
|
||||
/ "flatpak"
|
||||
/ "app"
|
||||
/ "org.evil.App"
|
||||
/ "x86_64"
|
||||
/ "stable"
|
||||
/ "active"
|
||||
)
|
||||
active.mkdir(parents=True)
|
||||
secret = tmp_path / "root-only-secret"
|
||||
secret.write_text("ROOTSECRET\n", encoding="utf-8")
|
||||
(active / "origin").symlink_to(secret)
|
||||
|
||||
apps = a.find_user_flatpaks(str(home), user="alice")
|
||||
|
||||
assert len(apps) == 1
|
||||
assert apps[0].name == "org.evil.App"
|
||||
assert apps[0].remote is None
|
||||
|
||||
|
||||
def test_user_flatpak_repo_config_symlink_is_not_read(tmp_path: Path):
|
||||
import enroll.accounts as a
|
||||
|
||||
home = tmp_path / "home"
|
||||
flatpak_root = home / ".local" / "share" / "flatpak"
|
||||
(flatpak_root / "repo").mkdir(parents=True)
|
||||
secret = tmp_path / "root-only-secret"
|
||||
secret.write_text(
|
||||
'[remote "ROOTSECRET"]\nurl=https://evil.example/\n', encoding="utf-8"
|
||||
)
|
||||
(flatpak_root / "repo" / "config").symlink_to(secret)
|
||||
|
||||
assert a.find_user_flatpak_remotes(str(home), user="alice") == []
|
||||
|
||||
|
||||
def test_user_flatpak_tree_with_symlinked_parent_is_not_walked(tmp_path: Path):
|
||||
import enroll.accounts as a
|
||||
|
||||
home = tmp_path / "home"
|
||||
real = tmp_path / "real-flatpak"
|
||||
active = (
|
||||
real
|
||||
/ "share"
|
||||
/ "flatpak"
|
||||
/ "app"
|
||||
/ "org.evil.App"
|
||||
/ "x86_64"
|
||||
/ "stable"
|
||||
/ "active"
|
||||
)
|
||||
active.mkdir(parents=True)
|
||||
(active / "origin").write_text("evil\n", encoding="utf-8")
|
||||
home.mkdir()
|
||||
(home / ".local").symlink_to(real, target_is_directory=True)
|
||||
|
||||
assert a.find_user_flatpaks(str(home), user="alice") == []
|
||||
|
|
|
|||
|
|
@ -444,3 +444,25 @@ def test_extra_paths_collector_records_include_path_that_is_symlink(tmp_path):
|
|||
(str(link_root), str(real_root), "user_include_link")
|
||||
]
|
||||
assert result.managed_files == []
|
||||
|
||||
|
||||
def test_extra_paths_collector_skips_directory_through_symlinked_parent(tmp_path):
|
||||
real_root = tmp_path / "real"
|
||||
child = real_root / "child"
|
||||
child.mkdir(parents=True)
|
||||
(child / "inside.conf").write_text("do-not-follow", encoding="utf-8")
|
||||
link_root = tmp_path / "linked-root"
|
||||
link_root.symlink_to(real_root, target_is_directory=True)
|
||||
|
||||
include_path = str(link_root / "child")
|
||||
ctx = _context(tmp_path, include=[include_path])
|
||||
result = ExtraPathsCollector(
|
||||
ctx,
|
||||
seen_by_role={},
|
||||
already_all=set(),
|
||||
include_paths=[include_path],
|
||||
).collect()
|
||||
|
||||
assert result.managed_dirs == []
|
||||
assert result.managed_files == []
|
||||
assert result.managed_links == []
|
||||
|
|
|
|||
|
|
@ -114,10 +114,10 @@ def test_manifest_uses_jinjaturtle_templates_and_does_not_copy_raw(
|
|||
def fake_run_jinjaturtle(
|
||||
jt_exe: str, src_path: str, *, role_name: str, force_format=None
|
||||
):
|
||||
assert role_name == "foo"
|
||||
assert role_name == "foo_jt_etc_foo_ini"
|
||||
return JinjifyResult(
|
||||
template_text="[main]\nkey = {{ foo_key }}\n",
|
||||
vars_text="foo_key: 1\n",
|
||||
template_text=f"[main]\nkey = {{{{ {role_name}_key }}}}\n",
|
||||
vars_text=f"{role_name}_key: 1\n",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(jinjaturtle_mod, "run_jinjaturtle", fake_run_jinjaturtle)
|
||||
|
|
@ -134,7 +134,7 @@ def test_manifest_uses_jinjaturtle_templates_and_does_not_copy_raw(
|
|||
|
||||
# Defaults should include jinjaturtle vars.
|
||||
defaults = (role_dir / "defaults" / "main.yml").read_text(encoding="utf-8")
|
||||
assert "foo_key: 1" in defaults
|
||||
assert "foo_jt_etc_foo_ini_key: 1" in defaults
|
||||
|
||||
|
||||
def test_openssh_paths_are_jinjaturtle_supported_and_forced_to_ssh() -> None:
|
||||
|
|
@ -188,14 +188,14 @@ def test_jinjify_managed_files_namespaces_multiple_templates(
|
|||
|
||||
assert templated == {"etc/foo/a.yaml", "etc/foo/b.yaml"}
|
||||
assert calls == [
|
||||
("a.yaml", "foo_etc_foo_a_yaml"),
|
||||
("b.yaml", "foo_etc_foo_b_yaml"),
|
||||
("a.yaml", "foo_jt_etc_foo_a_yaml"),
|
||||
("b.yaml", "foo_jt_etc_foo_b_yaml"),
|
||||
]
|
||||
assert "foo_etc_foo_a_yaml_ignore: []" in vars_text
|
||||
assert "foo_etc_foo_b_yaml_ignore: []" in vars_text
|
||||
assert "foo_jt_etc_foo_a_yaml_ignore: []" in vars_text
|
||||
assert "foo_jt_etc_foo_b_yaml_ignore: []" in vars_text
|
||||
assert (template_root / "etc" / "foo" / "a.yaml.j2").read_text(
|
||||
encoding="utf-8"
|
||||
) == "ignore: {{ foo_etc_foo_a_yaml_ignore }}\n"
|
||||
) == "ignore: {{ foo_jt_etc_foo_a_yaml_ignore }}\n"
|
||||
|
||||
|
||||
def test_jinjify_managed_files_rejects_templates_with_missing_defaults(
|
||||
|
|
@ -233,6 +233,80 @@ def test_jinjify_managed_files_rejects_templates_with_missing_defaults(
|
|||
assert not (template_root / "etc" / "foo" / "pdk.yaml.j2").exists()
|
||||
|
||||
|
||||
def test_jinjify_managed_files_always_namespaces_single_template(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
from enroll.jinjaturtle import jinjify_managed_files
|
||||
|
||||
bundle = tmp_path / "bundle"
|
||||
template_root = tmp_path / "templates"
|
||||
artifact = bundle / "artifacts" / "foo" / "etc" / "foo.yaml"
|
||||
artifact.parent.mkdir(parents=True, exist_ok=True)
|
||||
artifact.write_text("managed_files: []\n", encoding="utf-8")
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_run_jinjaturtle(jt_exe, src_path, *, role_name, force_format=None):
|
||||
calls.append(role_name)
|
||||
return JinjifyResult(
|
||||
template_text=f"managed_files: {{{{ {role_name}_managed_files }}}}\n",
|
||||
vars_text=f"{role_name}_managed_files: []\n",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(jinjaturtle_mod, "run_jinjaturtle", fake_run_jinjaturtle)
|
||||
|
||||
templated, vars_text = jinjify_managed_files(
|
||||
bundle,
|
||||
"foo",
|
||||
template_root,
|
||||
[{"path": "/etc/foo.yaml", "src_rel": "etc/foo.yaml"}],
|
||||
jt_exe="jinjaturtle",
|
||||
jt_enabled=True,
|
||||
overwrite_templates=True,
|
||||
role_name="foo",
|
||||
)
|
||||
|
||||
assert templated == {"etc/foo.yaml"}
|
||||
assert calls == ["foo_jt_etc_foo_yaml"]
|
||||
assert "foo_jt_etc_foo_yaml_managed_files: []" in vars_text
|
||||
assert "foo_managed_files:" not in vars_text
|
||||
|
||||
|
||||
def test_jinjify_managed_files_rejects_reserved_role_variable_collision(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
from enroll.jinjaturtle import jinjify_managed_files
|
||||
|
||||
bundle = tmp_path / "bundle"
|
||||
template_root = tmp_path / "templates"
|
||||
artifact = bundle / "artifacts" / "foo" / "etc" / "foo.yaml"
|
||||
artifact.parent.mkdir(parents=True, exist_ok=True)
|
||||
artifact.write_text("managed_files: []\n", encoding="utf-8")
|
||||
|
||||
def fake_run_jinjaturtle(jt_exe, src_path, *, role_name, force_format=None):
|
||||
return JinjifyResult(
|
||||
template_text="managed_files: {{ foo_managed_files }}\n",
|
||||
vars_text="foo_managed_files: []\n",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(jinjaturtle_mod, "run_jinjaturtle", fake_run_jinjaturtle)
|
||||
|
||||
templated, vars_text = jinjify_managed_files(
|
||||
bundle,
|
||||
"foo",
|
||||
template_root,
|
||||
[{"path": "/etc/foo.yaml", "src_rel": "etc/foo.yaml"}],
|
||||
jt_exe="jinjaturtle",
|
||||
jt_enabled=True,
|
||||
overwrite_templates=True,
|
||||
role_name="foo",
|
||||
)
|
||||
|
||||
assert templated == set()
|
||||
assert vars_text == ""
|
||||
assert not (template_root / "etc" / "foo.yaml.j2").exists()
|
||||
|
||||
|
||||
def test_jinjify_artifact_rejects_unsafe_src_rel(monkeypatch, tmp_path: Path):
|
||||
from enroll.jinjaturtle import jinjify_artifact
|
||||
|
||||
|
|
|
|||
|
|
@ -338,3 +338,37 @@ def test_path_filter_with_include_patterns():
|
|||
patterns = pf_filter.iter_include_patterns()
|
||||
assert len(patterns) == 1
|
||||
assert patterns[0].kind == "glob"
|
||||
|
||||
|
||||
def test_expand_includes_skips_file_through_symlinked_parent(tmp_path: Path):
|
||||
from enroll.pathfilter import compile_path_pattern, expand_includes
|
||||
|
||||
real = tmp_path / "real"
|
||||
(real / "child").mkdir(parents=True)
|
||||
target = real / "child" / "secret.conf"
|
||||
target.write_text("secret", encoding="utf-8")
|
||||
link = tmp_path / "link"
|
||||
link.symlink_to(real, target_is_directory=True)
|
||||
|
||||
paths, _notes = expand_includes(
|
||||
[compile_path_pattern(str(link / "child" / "secret.conf"))], max_files=10
|
||||
)
|
||||
|
||||
assert paths == []
|
||||
|
||||
|
||||
def test_expand_includes_skips_directory_through_symlinked_parent(tmp_path: Path):
|
||||
from enroll.pathfilter import compile_path_pattern, expand_includes
|
||||
|
||||
real = tmp_path / "real"
|
||||
(real / "child").mkdir(parents=True)
|
||||
target = real / "child" / "secret.conf"
|
||||
target.write_text("secret", encoding="utf-8")
|
||||
link = tmp_path / "link"
|
||||
link.symlink_to(real, target_is_directory=True)
|
||||
|
||||
paths, _notes = expand_includes(
|
||||
[compile_path_pattern(str(link / "child"))], max_files=10
|
||||
)
|
||||
|
||||
assert paths == []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue