Commit 5c73cfb
0
plików
+0
dodanych
-0
usuniętych
@@ -599,6 +599,47 @@ STAGING_DIR = "/.pag_staging" # na tej samej partycji co / (unikamy E
599
599
PKG_EXT = ".pag"
600
600
REPO_CACHE_TTL = 3600
601
601
MAX_PKG_SIZE = 2 * 1024 * 1024 * 1024 # 2 GB – maksymalny rozmiar paczki
602
+
603
+# ── UPRZYWILEJOWANE PLIKI ────────────────────────────────────────────────────
604
+# Domyślnie pag zdejmuje bit SUID i wymusza właściciela root:root. Niektóre
605
+# programy MUSZĄ jednak zachować setuid/setgid albo grupę inną niż root
606
+# (/usr/bin/sudo, /usr/bin/passwd, /usr/bin/unix_chkpwd,
607
+# /usr/lib/dbus-daemon-launch-helper). Bez tego tracą możliwość działania
608
+# po aktualizacji przez pag (np. użytkownik nie zmieni hasła).
609
+#
610
+# Bezpieczeństwo: zawartość pakietu jest uwierzytelniona (podpis GPG repo +
611
+# przypięty fingerprint + SHA256 całego pakietu), więc metadane w nim są
612
+# zaufane tak samo jak same pliki. Zachowujemy uprawnienia WYŁĄCZNIE dla
613
+# ścieżek z tej listy (albo z pola `privileged:` receptury, które pagbuild
614
+# zapisuje w metadata.json).
615
+PRIVILEGED_PATHS = frozenset({
616
+ "/usr/bin/cdda2wav", "/usr/bin/cdrecord",
617
+ "/usr/bin/chage", "/usr/bin/expiry",
618
+ "/usr/bin/fcrondyn", "/usr/bin/fcronsighup", "/usr/bin/fcrontab",
619
+ "/usr/bin/firejail", "/usr/bin/fping",
620
+ "/usr/bin/fusermount", "/usr/bin/fusermount3",
621
+ "/usr/bin/gpasswd", "/usr/bin/ksu", "/usr/bin/lockfile",
622
+ "/usr/bin/mount", "/usr/bin/mount.nfs",
623
+ "/usr/bin/newgidmap", "/usr/bin/newuidmap",
624
+ "/usr/bin/passwd", "/usr/bin/ping", "/usr/bin/ping6",
625
+ "/usr/bin/procmail", "/usr/bin/readcd", "/usr/bin/rscsi",
626
+ "/usr/bin/screen", "/usr/bin/screen-5.0.2",
627
+ "/usr/bin/sg", "/usr/bin/su", "/usr/bin/sudo", "/usr/bin/traceroute",
628
+ "/usr/bin/umount", "/usr/bin/unix_chkpwd", "/usr/bin/wall",
629
+ "/usr/lib/dbus-daemon-launch-helper",
630
+ "/usr/lib/httpd/suexec",
631
+ "/usr/lib/utempter/utempter",
632
+ "/usr/lib/xscreensaver/xscreensaver-auth",
633
+ "/usr/libexec/libgtop_server2",
634
+ "/usr/libexec/qemu-bridge-helper",
635
+ "/usr/libexec/ssh-keysign",
636
+ "/usr/libexec/xf86-video-intel-backlight-helper",
637
+ "/opt/google/chrome/chrome-sandbox",
638
+ "/opt/gitkraken/chrome-sandbox",
639
+ "/opt/VirtualBox/VBoxHeadless", "/opt/VirtualBox/VBoxNetAdpCtl",
640
+ "/opt/VirtualBox/VBoxNetDHCP", "/opt/VirtualBox/VBoxVolInfo",
641
+ "/opt/VirtualBox/VirtualBoxVM",
642
+})
602
643
ALLOWED_PKG_RE = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9._+@-]*$')
603
644
604
645
# Bezpieczeństwo / audyt
@@ -1080,7 +1121,8 @@ def _check_downgrade(pkg_name, new_ver, installed_db):
1080
1121
return False
1081
1122
return True
1082
1123
1083
-def _safe_extractall(tar: tarfile.TarFile, dest: str, *, preserve_perms: bool = True):
1124
+def _safe_extractall(tar: tarfile.TarFile, dest: str, *, preserve_perms: bool = True,
1125
+ privileged=frozenset()):
1084
1126
"""
1085
1127
Bezpieczne rozpakowanie archiwum tar z ochroną przed Directory Traversal.
1086
1128
@@ -1160,7 +1202,10 @@ def _safe_extractall(tar: tarfile.TarFile, dest: str, *, preserve_perms: bool =
1160
1202
except Exception as e:
1161
1203
print(f" ⚠ Nie rozpakowano {name}: {e}")
1162
1204
continue
1163
- _strip_suid(target)
1205
+ # Pliki z listy uprzywilejowanej zachowują SUID/SGID; reszta jest
1206
+ # czyszczona (ochrona przed SUID niespodziewanym w pakiecie).
1207
+ if "/" + os.path.relpath(target, dest_real) not in privileged:
1208
+ _strip_suid(target)
1164
1209
1165
1210
1166
1211
def _sha256_file(path: str) -> str:
@@ -1258,7 +1303,7 @@ def save_json(path, data):
1258
1303
class PackageInfo:
1259
1304
__slots__ = ("name","version","release","description","dependencies",
1260
1305
"size_bytes","sha256","gpg_fp","repo_url","filename","provides","license",
1261
- "provides_so","requires_so")
1306
+ "provides_so","requires_so","privileged")
1262
1307
def __init__(self, d, repo=""):
1263
1308
self.name = d.get("name","?")
1264
1309
self.version = d.get("version","0")
@@ -1274,6 +1319,8 @@ class PackageInfo:
1274
1319
self.license = d.get("license", []) or []
1275
1320
self.provides_so = d.get("provides_so", []) or []
1276
1321
self.requires_so = d.get("requires_so", []) or []
1322
+ # Ścieżki, którym wolno zachować setuid/setgid i grupę (z receptury).
1323
+ self.privileged = d.get("privileged", []) or []
1277
1324
1278
1325
# =============================================================================
1279
1326
# REPOZYTORIA (cache, ETag, GPG)
@@ -1794,7 +1841,8 @@ def _install_file(src: str, rel: str, data_staging: str, sums: dict,
1794
1841
staging: str, journal: list, installed_files: list,
1795
1842
deploy_dir: str = "", backup_dir: str = "",
1796
1843
backup_journal: Optional[list] = None,
1797
- old_checksums: Optional[dict] = None) -> bool:
1844
+ old_checksums: Optional[dict] = None,
1845
+ privileged=frozenset()) -> bool:
1798
1846
"""
1799
1847
Instaluje pojedynczy plik (zwykły lub symlink).
1800
1848
Obsługuje: cross-device rename, symlinki, weryfikację SHA256.
@@ -1916,12 +1964,23 @@ def _install_file(src: str, rel: str, data_staging: str, sums: dict,
1916
1964
# Zachowuje bity uprawnień (SUID/SGID/sticky) – NIE używamy filter='data'.
1917
1965
_safe_rename(src, dst)
1918
1966
1919
- # Wymuś właściciela root:root. UWAGA: os.chown() NIE czyści bitów SUID/SGID.
1920
- try:
1921
- os.chown(dst, 0, 0)
1922
- except (OSError, PermissionError):
1923
- # Na niektórych systemach plików (tmpfs, fat) chown może się nie powieść
1924
- pass
1967
+ if "/" + rel in privileged:
1968
+ # Plik uprzywilejowany: zostaw właściciela, grupę i bity z pakietu
1969
+ # (np. sudo 4755 root:root, qemu-bridge-helper 4750 root:kvm).
1970
+ try:
1971
+ st = os.stat(dst)
1972
+ if st.st_mode & 0o6000:
1973
+ _audit(f"privileged {dst} mode={oct(st.st_mode & 0o7777)} "
1974
+ f"uid={st.st_uid} gid={st.st_gid}")
1975
+ except OSError:
1976
+ pass
1977
+ else:
1978
+ # Wymuś właściciela root:root. UWAGA: os.chown() NIE czyści bitów SUID/SGID.
1979
+ try:
1980
+ os.chown(dst, 0, 0)
1981
+ except (OSError, PermissionError):
1982
+ # Na niektórych systemach plików (tmpfs, fat) chown może się nie powieść
1983
+ pass
1925
1984
1926
1985
journal.append(("file", src, dst))
1927
1986
installed_files.append({
@@ -1942,6 +2001,8 @@ def _atomic_install(pkg_path: str, pkg: PackageInfo, deploy_dir: str = "",
1942
2001
Zwraca (success, [lista plików z SHA256], [(backup_path, dst), ...]).
1943
2002
"""
1944
2003
staging = tempfile.mkdtemp(dir=STAGING_DIR, prefix=f".staging-{pkg.name}-")
2004
+ # Wbudowana lista + ścieżki zadeklarowane przez recepturę (metadata.json).
2005
+ priv = PRIVILEGED_PATHS | frozenset(pkg.privileged or ())
1945
2006
journal = []
1946
2007
installed_files = []
1947
2008
backup_journal: List[Tuple[str, str]] = []
@@ -1960,7 +2021,7 @@ def _atomic_install(pkg_path: str, pkg: PackageInfo, deploy_dir: str = "",
1960
2021
data_staging = os.path.join(staging, "data")
1961
2022
os.makedirs(data_staging, exist_ok=True)
1962
2023
with tarfile.open(data_tar, "r:xz") as tf:
1963
- _safe_extractall(tf, data_staging)
2024
+ _safe_extractall(tf, data_staging, privileged=priv)
1964
2025
1965
2026
# Wczytaj sums.json
1966
2027
sums_path = os.path.join(data_staging, "sums.json")
@@ -1998,7 +2059,8 @@ def _atomic_install(pkg_path: str, pkg: PackageInfo, deploy_dir: str = "",
1998
2059
ok = _install_file(src, rel, data_staging, sums,
1999
2060
staging, journal, installed_files, deploy_dir,
2000
2061
backup_dir, backup_journal,
2001
- old_checksums=old_checksums)
2062
+ old_checksums=old_checksums,
2063
+ privileged=priv)
2002
2064
if not ok:
2003
2065
# Cofnij wszystkie operacje
2004
2066
_rollback_journal(journal, staging)