@@ -10,6 +10,7 @@ const yauzl = require('yauzl');
1010const { exec, execSync, spawn, spawnSync } = require ( 'child_process' ) ;
1111const http = require ( 'http' ) ;
1212const https = require ( 'https' ) ;
13+ const net = require ( 'net' ) ;
1314const readline = require ( 'readline' ) ;
1415const {
1516 expandHomePath,
@@ -7315,20 +7316,57 @@ function cmdClaude(baseUrl, apiKey, model, silent = false) {
73157316function commandExists ( command , args = '' ) {
73167317 const cmd = typeof command === 'string' ? command . trim ( ) : '' ;
73177318 const argText = typeof args === 'string' ? args . trim ( ) : '' ;
7318- if ( ! cmd || ! / ^ [ A - Z a - z 0 - 9 . _ - ] + $ / . test ( cmd ) ) {
7319+ if ( ! cmd || cmd . includes ( '\0' ) || / [ \r \n ] / . test ( cmd ) ) {
73197320 return false ;
73207321 }
7321- if ( argText && / [ \r \n ; & | < > ` $ ] / . test ( argText ) ) {
7322- return false ;
7322+ const argv = argText ? argText . split ( / \s + / g) . filter ( Boolean ) : [ ] ;
7323+ const hasSeparators = cmd . includes ( '/' ) || cmd . includes ( '\\' ) ;
7324+ const useShell = process . platform === 'win32' && ! hasSeparators ;
7325+ if ( useShell ) {
7326+ if ( ! / ^ [ A - Z a - z 0 - 9 . _ - ] + $ / . test ( cmd ) ) return false ;
7327+ if ( argText && / [ \r \n ; & | < > ` $ ] / . test ( argText ) ) return false ;
73237328 }
73247329 try {
7325- execSync ( `${ cmd } ${ argText ? ` ${ argText } ` : '' } ` , { stdio : 'ignore' , shell : process . platform === 'win32' } ) ;
7326- return true ;
7327- } catch ( e ) {
7330+ const probe = spawnSync ( cmd , argv , {
7331+ stdio : 'ignore' ,
7332+ windowsHide : true ,
7333+ timeout : 5000 ,
7334+ shell : useShell
7335+ } ) ;
7336+ return probe . status === 0 ;
7337+ } catch ( _ ) {
73287338 return false ;
73297339 }
73307340}
73317341
7342+ function isPrivateNetworkHost ( hostname ) {
7343+ const host = typeof hostname === 'string' ? hostname . trim ( ) . toLowerCase ( ) : '' ;
7344+ if ( ! host ) return true ;
7345+ if ( host === 'localhost' ) return true ;
7346+ const ipVer = net . isIP ( host ) ;
7347+ if ( ! ipVer ) {
7348+ return false ;
7349+ }
7350+ if ( ipVer === 4 ) {
7351+ const parts = host . split ( '.' ) . map ( ( x ) => parseInt ( x , 10 ) ) ;
7352+ if ( parts . length !== 4 || parts . some ( ( x ) => ! Number . isFinite ( x ) ) ) return true ;
7353+ const [ a , b ] = parts ;
7354+ if ( a === 10 ) return true ;
7355+ if ( a === 127 ) return true ;
7356+ if ( a === 169 && b === 254 ) return true ;
7357+ if ( a === 192 && b === 168 ) return true ;
7358+ if ( a === 172 && b >= 16 && b <= 31 ) return true ;
7359+ return false ;
7360+ }
7361+ if ( ipVer === 6 ) {
7362+ if ( host === '::1' ) return true ;
7363+ if ( host . startsWith ( 'fe80:' ) ) return true ;
7364+ if ( host . startsWith ( 'fc' ) || host . startsWith ( 'fd' ) ) return true ;
7365+ return false ;
7366+ }
7367+ return false ;
7368+ }
7369+
73327370function detectPreferredPackageManager ( ) {
73337371 const userAgent = typeof process . env . npm_config_user_agent === 'string'
73347372 ? process . env . npm_config_user_agent . trim ( ) . toLowerCase ( )
@@ -8643,6 +8681,20 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
86438681 if ( ! baseUrl ) {
86448682 result = { error : 'Base URL is required' } ;
86458683 } else {
8684+ const remoteAddr = req && req . socket ? req . socket . remoteAddress : '' ;
8685+ const requesterIsLoopback = ! remoteAddr
8686+ || remoteAddr === '127.0.0.1'
8687+ || remoteAddr === '::1'
8688+ || remoteAddr === '::ffff:127.0.0.1' ;
8689+ if ( ! requesterIsLoopback ) {
8690+ try {
8691+ const parsedUrl = new URL ( baseUrl ) ;
8692+ if ( isPrivateNetworkHost ( parsedUrl . hostname || '' ) ) {
8693+ result = { error : 'Refusing to access private network baseUrl from non-loopback request' } ;
8694+ break ;
8695+ }
8696+ } catch ( _ ) { }
8697+ }
86468698 const res = await fetchModelsFromBaseUrl ( baseUrl , apiKey ) ;
86478699 if ( res . error ) {
86488700 result = { error : res . error , models : [ ] , source : 'remote' } ;
@@ -9230,7 +9282,14 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
92309282 } ) ;
92319283 return ;
92329284 }
9233-
9285+ const allowLegacy = process . env . CODEXMATE_ALLOW_LEGACY_DOWNLOAD === '1' ;
9286+ const remoteAddr = req && req . socket ? req . socket . remoteAddress : '' ;
9287+ const isLoopback = ! remoteAddr || remoteAddr === '127.0.0.1' || remoteAddr === '::1' || remoteAddr === '::ffff:127.0.0.1' ;
9288+ if ( ! allowLegacy || ! isLoopback ) {
9289+ res . writeHead ( 404 , { 'Content-Type' : 'text/plain; charset=utf-8' } ) ;
9290+ res . end ( 'Not Found' ) ;
9291+ return ;
9292+ }
92349293 const tempDir = os . tmpdir ( ) ;
92359294 const legacyFilePath = path . join ( tempDir , decodedFileName ) ;
92369295 if ( ! isPathInside ( legacyFilePath , tempDir ) ) {
@@ -9304,8 +9363,12 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
93049363 }
93059364 console . log ( ' 退出: Ctrl+C\n' ) ;
93069365 if ( isAnyAddressHost ( host ) ) {
9307- console . warn ( '! 安全提示: 当前监听所有网卡(无鉴权)。' ) ;
9308- console . warn ( ' 建议仅在可信网络使用,或改用 --host 127.0.0.1。' ) ;
9366+ const tokenEnabled = typeof process . env . CODEXMATE_HTTP_TOKEN === 'string' && process . env . CODEXMATE_HTTP_TOKEN . trim ( ) . length > 0 ;
9367+ console . warn ( `! 安全提示: 当前监听所有网卡(${ tokenEnabled ? '已启用鉴权' : '无鉴权' } )。` ) ;
9368+ if ( ! tokenEnabled ) {
9369+ console . warn ( ' 建议仅在可信网络使用,或改用 --host 127.0.0.1。' ) ;
9370+ console . warn ( ' 如需远程访问,请设置 CODEXMATE_HTTP_TOKEN。' ) ;
9371+ }
93099372 }
93109373
93119374 if ( willOpenBrowser ) {
0 commit comments