44// Runs automatically via `postinstall` in package.json.
55
66const https = require ( 'https' ) ;
7+ const crypto = require ( 'crypto' ) ;
78const fs = require ( 'fs' ) ;
89const path = require ( 'path' ) ;
910const os = require ( 'os' ) ;
10- const { execSync } = require ( 'child_process' ) ;
11+ const { execFileSync } = require ( 'child_process' ) ;
1112
1213const REPO = 'DeusData/codebase-memory-mcp' ;
1314const VERSION = require ( './package.json' ) . version ;
@@ -30,12 +31,25 @@ function getArch() {
3031 }
3132}
3233
34+ // Security: only follow HTTPS URLs (defense-in-depth).
35+ function validateUrl ( url ) {
36+ if ( ! url . startsWith ( 'https://' ) ) {
37+ throw new Error ( `Refusing non-HTTPS URL: ${ url } ` ) ;
38+ }
39+ }
40+
3341function download ( url , dest ) {
42+ validateUrl ( url ) ;
3443 return new Promise ( ( resolve , reject ) => {
35- function follow ( u ) {
44+ function follow ( u , depth ) {
45+ if ( depth > 5 ) return reject ( new Error ( 'Too many redirects' ) ) ;
46+ validateUrl ( u ) ;
3647 https . get ( u , ( res ) => {
3748 if ( res . statusCode === 301 || res . statusCode === 302 ) {
38- return follow ( res . headers . location ) ;
49+ const loc = res . headers . location ;
50+ if ( ! loc ) return reject ( new Error ( 'Redirect with no location' ) ) ;
51+ const next = loc . startsWith ( '/' ) ? new URL ( loc , u ) . href : loc ;
52+ return follow ( next , depth + 1 ) ;
3953 }
4054 if ( res . statusCode !== 200 ) {
4155 return reject ( new Error ( `HTTP ${ res . statusCode } for ${ u } ` ) ) ;
@@ -46,10 +60,38 @@ function download(url, dest) {
4660 file . on ( 'error' , reject ) ;
4761 } ) . on ( 'error' , reject ) ;
4862 }
49- follow ( url ) ;
63+ follow ( url , 0 ) ;
5064 } ) ;
5165}
5266
67+ // Fetch checksums.txt and verify the archive hash.
68+ async function verifyChecksum ( archivePath , archiveName ) {
69+ const url = `https://github.com/${ REPO } /releases/download/v${ VERSION } /checksums.txt` ;
70+ const tmpChecksums = archivePath + '.checksums' ;
71+ try {
72+ await download ( url , tmpChecksums ) ;
73+ const lines = fs . readFileSync ( tmpChecksums , 'utf-8' ) . split ( '\n' ) ;
74+ const match = lines . find ( ( l ) => l . includes ( archiveName ) ) ;
75+ if ( ! match ) return ; // checksum line not found — non-fatal
76+ const expected = match . split ( / \s + / ) [ 0 ] ;
77+ const actual = crypto
78+ . createHash ( 'sha256' )
79+ . update ( fs . readFileSync ( archivePath ) )
80+ . digest ( 'hex' ) ;
81+ if ( expected !== actual ) {
82+ throw new Error (
83+ `Checksum mismatch for ${ archiveName } :\n expected: ${ expected } \n actual: ${ actual } ` ,
84+ ) ;
85+ }
86+ process . stdout . write ( 'codebase-memory-mcp: checksum verified.\n' ) ;
87+ } catch ( err ) {
88+ if ( err . message . startsWith ( 'Checksum mismatch' ) ) throw err ;
89+ // Non-fatal: checksum unavailable (network issue, pre-release, etc.)
90+ } finally {
91+ try { fs . unlinkSync ( tmpChecksums ) ; } catch ( _ ) { /* ignore */ }
92+ }
93+ }
94+
5395async function main ( ) {
5496 const platform = getPlatform ( ) ;
5597 const arch = getArch ( ) ;
@@ -73,24 +115,33 @@ async function main() {
73115
74116 try {
75117 await download ( url , tmpArchive ) ;
118+ await verifyChecksum ( tmpArchive , archive ) ;
76119
120+ // Extract using execFileSync (array args — no shell injection).
77121 if ( ext === 'tar.gz' ) {
78- execSync ( ` tar -xzf " ${ tmpArchive } " -C " ${ tmpDir } "` ) ;
122+ execFileSync ( ' tar' , [ ' -xzf' , tmpArchive , '-C' , tmpDir , '--no-same-owner' ] ) ;
79123 } else {
80- execSync (
81- `powershell -NoProfile -Command "Expand-Archive -Path '${ tmpArchive } ' -DestinationPath '${ tmpDir } ' -Force"` ,
82- ) ;
124+ execFileSync ( 'powershell' , [
125+ '-NoProfile' , '-Command' ,
126+ `Expand-Archive -Path '${ tmpArchive } ' -DestinationPath '${ tmpDir } ' -Force` ,
127+ ] ) ;
83128 }
84129
130+ // Validate extracted path doesn't escape tmpDir (tar-slip defense).
85131 const extracted = path . join ( tmpDir , binName ) ;
132+ const resolvedExtracted = path . resolve ( extracted ) ;
133+ const resolvedTmpDir = path . resolve ( tmpDir ) ;
134+ if ( ! resolvedExtracted . startsWith ( resolvedTmpDir + path . sep ) ) {
135+ throw new Error ( `Path traversal detected in archive: ${ binName } ` ) ;
136+ }
86137 if ( ! fs . existsSync ( extracted ) ) {
87138 throw new Error ( `Binary not found after extraction at ${ extracted } ` ) ;
88139 }
89140
90141 fs . copyFileSync ( extracted , binPath ) ;
91142 fs . chmodSync ( binPath , 0o755 ) ;
92143
93- process . stdout . write ( ` codebase-memory-mcp: ready.\n` ) ;
144+ process . stdout . write ( ' codebase-memory-mcp: ready.\n' ) ;
94145 } finally {
95146 fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
96147 }
0 commit comments