Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/selftest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ jobs:
tools/wolfglass-sync \
tests/test_gen_sbom.py \
tests/test_sbom.py \
tests/test_sbom_identity.py
tests/test_sbom_identity.py \
tests/test_wolfglass_sync.py

- name: Run generator unit tests
run: python -m unittest tests/test_gen_sbom.py
Expand All @@ -61,5 +62,8 @@ jobs:
- name: Run SBOM identity tests
run: python -m unittest tests/test_sbom_identity.py

- name: Run wolfglass-sync tests
run: python -m unittest tests/test_wolfglass_sync.py

- name: Run self-test
run: python tests/test_sbom.py
104 changes: 104 additions & 0 deletions tests/test_wolfglass_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Tests for tools/wolfglass-sync, focused on the --subdir containment guard.

--subdir is joined onto --dest and then written into, so an absolute value or
one containing .. must be refused rather than allowed to place/overwrite files
outside the product's vendoring path."""

import os
import subprocess
import sys
import tempfile
import unittest

REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SYNC = os.path.join(REPO, "tools", "wolfglass-sync")


def _run(dest, subdir):
return subprocess.run(
[sys.executable, SYNC, "--dest", dest, "--subdir", subdir],
capture_output=True, text=True)


class TestSubdirContainment(unittest.TestCase):
def test_relative_subdir_copies_inside_dest(self):
with tempfile.TemporaryDirectory() as dest:
r = _run(dest, "tools/sbom")
self.assertEqual(r.returncode, 0, r.stderr)
# gen-sbom is part of share/, so it must land under the subdir.
self.assertTrue(
os.path.isfile(os.path.join(dest, "tools", "sbom", "gen-sbom")),
os.listdir(dest))

def test_absolute_subdir_rejected(self):
with tempfile.TemporaryDirectory() as dest, \
tempfile.TemporaryDirectory() as outside:
target = os.path.join(outside, "pwned")
r = _run(dest, target) # absolute path
self.assertNotEqual(r.returncode, 0)
self.assertIn("--subdir", r.stderr)
self.assertFalse(os.path.exists(target),
"absolute --subdir wrote outside --dest")

def test_dotdot_subdir_rejected(self):
with tempfile.TemporaryDirectory() as parent:
dest = os.path.join(parent, "product")
os.mkdir(dest)
r = _run(dest, "../escape")
self.assertNotEqual(r.returncode, 0)
self.assertIn("escapes --dest", r.stderr)
self.assertFalse(os.path.exists(os.path.join(parent, "escape")),
"../ --subdir wrote outside --dest")

def test_subdir_symlink_outside_rejected(self):
with tempfile.TemporaryDirectory() as dest, \
tempfile.TemporaryDirectory() as outside:
os.symlink(outside, os.path.join(dest, "link"))
r = _run(dest, "link")
self.assertNotEqual(r.returncode, 0)
self.assertIn("escapes --dest", r.stderr)
self.assertEqual(os.listdir(outside), [],
"symlinked --subdir wrote outside --dest")

def test_root_dest_accepts_nested_subdir(self):
with tempfile.TemporaryDirectory() as tmp:
sub = os.path.relpath(os.path.join(tmp, "a", "b"), os.sep)
r = _run(os.sep, sub)
self.assertEqual(r.returncode, 0, r.stderr)
self.assertTrue(
os.path.isfile(os.path.join(tmp, "a", "b", "gen-sbom")))


class TestSymlinkInsideDest(unittest.TestCase):
"""A symlink already present under --subdir must not redirect writes."""

def test_file_symlink_escape_refused(self):
with tempfile.TemporaryDirectory() as dest, \
tempfile.TemporaryDirectory() as outside:
victim = os.path.join(outside, "victim")
with open(victim, "w") as f:
f.write("original\n")
sbom = os.path.join(dest, "tools", "sbom")
os.makedirs(sbom)
os.symlink(victim, os.path.join(sbom, "gen-sbom"))
r = _run(dest, "tools/sbom")
self.assertNotEqual(r.returncode, 0)
with open(victim) as f:
self.assertEqual(f.read(), "original\n")
self.assertEqual(os.listdir(outside), ["victim"])

def test_dir_symlink_escape_refused(self):
with tempfile.TemporaryDirectory() as dest, \
tempfile.TemporaryDirectory() as outside:
sbom = os.path.join(dest, "tools", "sbom")
os.makedirs(sbom)
os.symlink(outside, os.path.join(sbom, "frontends"))
r = _run(dest, "tools/sbom")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(os.listdir(outside), [],
"symlinked dir under --subdir wrote outside")


if __name__ == "__main__":
unittest.main(verbosity=2)
31 changes: 31 additions & 0 deletions tools/wolfglass-sync
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ def iter_share_files(share_dir):
yield full, os.path.relpath(full, share_dir)


def is_inside(base, path):
"""True if absolute path equals base or lies beneath it."""
return os.path.commonpath([base, path]) == base


def source_revision(src_root):
"""Return the source git revision if available, else an empty string."""
try:
Expand Down Expand Up @@ -66,7 +71,19 @@ def main():
if not os.path.isdir(share_dir):
sys.exit(f"ERROR: no share/ directory at {share_dir}")

# --subdir is joined onto --dest and then written into. An absolute value
# (os.path.join discards --dest) or one containing .. would place and
# overwrite files outside the product's vendoring path, so require it to
# resolve to a location inside --dest.
if os.path.isabs(args.subdir):
sys.exit(f"ERROR: --subdir must be relative to --dest, got absolute "
f"{args.subdir!r}.")
dest_root = os.path.join(args.dest, args.subdir)
dest_abs = os.path.realpath(args.dest)
dest_root_abs = os.path.realpath(dest_root)
if not is_inside(dest_abs, dest_root_abs):
sys.exit(f"ERROR: --subdir {args.subdir!r} escapes --dest; it must "
f"resolve to a path inside {args.dest!r}.")
files = sorted(iter_share_files(share_dir), key=lambda p: p[1])

if args.check:
Expand Down Expand Up @@ -98,6 +115,20 @@ def main():
print("OK: vendored wolfGlass files match share/.")
return

# A symlink already present under dest_root (a file, or a directory on
# the way to one) would redirect a write outside it. Resolve every target
# before writing anything and refuse if one leaves dest_root.
targets = [rel for _, rel in files]
if os.path.isfile(version_file):
targets.append("VERSION")
if source_rev:
targets.append(".wolfglass-rev")
for rel in targets:
if not is_inside(dest_root_abs,
os.path.realpath(os.path.join(dest_root, rel))):
sys.exit(f"ERROR: {rel!r} under {dest_root!r} resolves outside "
f"it through a symlink; refusing to write.")

count = 0
for src, rel in files:
dst = os.path.join(dest_root, rel)
Expand Down
Loading