11"""Downloads the codebase-memory-mcp binary on first run, then exec's it."""
22
3+ import hashlib
34import os
45import sys
56import platform
@@ -36,16 +37,12 @@ def _safe_extract_tar(tf, dest: str) -> None:
3637 manual per-member path validation on older Pythons. Mitigates the
3738 classic tar-slip / Zip Slip vulnerability (CWE-22).
3839 """
39- # Python 3.12+: use the built-in 'data' filter which rejects absolute
40- # paths, '..' components, symlinks pointing outside dest, etc.
4140 if hasattr (tf , "extraction_filter" ) or sys .version_info >= (3 , 12 ):
4241 tf .extractall (dest , filter = "data" )
4342 return
4443
45- # Fallback for Python <3.12: validate each member before extracting.
4644 dest_abs = os .path .abspath (dest )
4745 for member in tf .getmembers ():
48- # Reject symlinks and hardlinks outright (they can escape dest).
4946 if member .issym () or member .islnk ():
5047 sys .exit (
5148 f"codebase-memory-mcp: refusing unsafe tar entry "
@@ -61,11 +58,7 @@ def _safe_extract_tar(tf, dest: str) -> None:
6158
6259
6360def _safe_extract_zip (zf , dest : str ) -> None :
64- """Extract a zipfile to dest, rejecting path-traversal entries.
65-
66- zipfile.ZipFile has no built-in extraction filter; mirrors the tar
67- fallback logic — validate each member before extracting.
68- """
61+ """Extract a zipfile to dest, rejecting path-traversal entries."""
6962 dest_abs = os .path .abspath (dest )
7063 for name in zf .namelist ():
7164 member_abs = os .path .abspath (os .path .join (dest_abs , name ))
@@ -77,6 +70,42 @@ def _safe_extract_zip(zf, dest: str) -> None:
7770 zf .extractall (dest )
7871
7972
73+ def _verify_checksum (archive_path : str , archive_name : str , version : str ) -> None :
74+ """Verify SHA256 checksum against checksums.txt from the release."""
75+ url = f"https://github.com/{ REPO } /releases/download/v{ version } /checksums.txt"
76+ try :
77+ _validate_url_scheme (url )
78+ with tempfile .NamedTemporaryFile (suffix = ".txt" , delete = False ) as tmp :
79+ tmp_path = tmp .name
80+ urllib .request .urlretrieve (url , tmp_path ) # noqa: S310 — scheme validated above
81+ with open (tmp_path ) as f :
82+ for line in f :
83+ if archive_name in line :
84+ expected = line .split ()[0 ]
85+ h = hashlib .sha256 ()
86+ with open (archive_path , "rb" ) as af :
87+ for chunk in iter (lambda : af .read (65536 ), b"" ):
88+ h .update (chunk )
89+ actual = h .hexdigest ()
90+ if expected != actual :
91+ sys .exit (
92+ f"codebase-memory-mcp: CHECKSUM MISMATCH for { archive_name } \n "
93+ f" expected: { expected } \n "
94+ f" actual: { actual } "
95+ )
96+ print ("codebase-memory-mcp: checksum verified." , file = sys .stderr )
97+ break
98+ except SystemExit :
99+ raise
100+ except Exception :
101+ pass # Non-fatal: checksum unavailable
102+ finally :
103+ try :
104+ os .unlink (tmp_path )
105+ except Exception :
106+ pass
107+
108+
80109def _version () -> str :
81110 try :
82111 from importlib .metadata import version
@@ -124,10 +153,8 @@ def _download(version: str) -> Path:
124153 os_name = _os_name ()
125154 arch = _arch ()
126155 ext = "zip" if os_name == "windows" else "tar.gz"
127- url = (
128- f"https://github.com/{ REPO } /releases/download/v{ version } "
129- f"/codebase-memory-mcp-{ os_name } -{ arch } .{ ext } "
130- )
156+ archive = f"codebase-memory-mcp-{ os_name } -{ arch } .{ ext } "
157+ url = f"https://github.com/{ REPO } /releases/download/v{ version } /{ archive } "
131158 _validate_url_scheme (url )
132159
133160 dest = _bin_path (version )
@@ -149,6 +176,8 @@ def _download(version: str) -> Path:
149176 f"See https://github.com/{ REPO } /releases for available versions."
150177 )
151178
179+ _verify_checksum (tmp_archive , archive , version )
180+
152181 if ext == "tar.gz" :
153182 import tarfile
154183 with tarfile .open (tmp_archive ) as tf :
@@ -161,7 +190,7 @@ def _download(version: str) -> Path:
161190 bin_name = "codebase-memory-mcp.exe" if os_name == "windows" else "codebase-memory-mcp"
162191 extracted = os .path .join (tmp , bin_name )
163192 if not os .path .exists (extracted ):
164- sys .exit (f "codebase-memory-mcp: binary not found after extraction" )
193+ sys .exit ("codebase-memory-mcp: binary not found after extraction" )
165194
166195 shutil .copy2 (extracted , dest )
167196 current = dest .stat ().st_mode
0 commit comments