@@ -75,7 +75,7 @@ public function extract($source, $target)
7575 $ result = $ this ->_extractByCommand ($ source , $ target );
7676 }
7777 if ($ result ) {
78- $ extractedPath = $ target . $ this ->topArchiveName ;
78+ $ extractedPath = rtrim ( $ target, " / \\" ) . DS . $ this ->topArchiveName ;
7979 $ Folder = new BcFolder ($ extractedPath );
8080 $ Folder ->chmod ( 0777 );
8181 if ($ this ->Zip ) $ this ->Zip ->close ();
@@ -97,14 +97,25 @@ public function extract($source, $target)
9797 */
9898 protected function _extractByPhpLib ($ source , $ target )
9999 {
100- if ($ this ->Zip ->open ($ source ) === true && $ this ->Zip ->extractTo ($ target )) {
100+ if ($ this ->Zip ->open ($ source ) !== true ) {
101+ return false ;
102+ }
103+ $ targetPath = $ this ->_normalizeTargetPath ($ target );
104+ if ($ targetPath === false ) {
105+ $ this ->Zip ->close ();
106+ return false ;
107+ }
108+ if (!$ this ->_validateZipEntries ($ targetPath )) {
109+ $ this ->Zip ->close ();
110+ return false ;
111+ }
112+ if ($ this ->Zip ->extractTo ($ target )) {
101113 $ archivePath = $ this ->Zip ->getNameIndex (0 );
102114 $ archivePathAry = explode ('/ ' , $ archivePath );
103115 $ this ->topArchiveName = $ archivePathAry [0 ];
104116 return true ;
105- } else {
106- return false ;
107117 }
118+ return false ;
108119 }
109120
110121 /**
@@ -124,7 +135,27 @@ protected function _extractByCommand($source, $target)
124135 return false ;
125136 }
126137 $ unzipCommand = $ return1 [0 ];
127- $ target = preg_replace ('/\/$/ ' , '' , $ target );
138+ $ targetPath = $ this ->_normalizeTargetPath ($ target );
139+ if ($ targetPath === false ) {
140+ return false ;
141+ }
142+ $ listCommand = $ unzipCommand . ' -Z -1 ' . $ this ->_escapePath ($ source );
143+ exec ($ listCommand . ' 2>&1 ' , $ entries , $ listStatus );
144+ if ($ listStatus !== 0 ) {
145+ $ this ->error = $ entries ;
146+ return false ;
147+ }
148+ foreach ($ entries as $ entry ) {
149+ $ entry = trim ($ entry );
150+ if ($ entry === '' ) {
151+ continue ;
152+ }
153+ if (!$ this ->_isZipEntrySafe ($ entry , $ targetPath )) {
154+ $ this ->error = __d ('baser_core ' , 'Invalid zip entry path: {0} ' , $ entry );
155+ return false ;
156+ }
157+ }
158+ $ target = rtrim ($ target , "/ \\" );
128159 $ command = $ unzipCommand . ' -o ' . $ this ->_escapePath ($ source ) . ' -d ' . $ this ->_escapePath ($ target );
129160 exec ($ command . ' 2>&1 ' , $ return2 );
130161 if (!empty ($ return2 [2 ])) {
@@ -158,6 +189,166 @@ protected function _escapePath($path)
158189 return implode (DS , $ pathAry );
159190 }
160191
192+ /**
193+ * ZIPエントリがターゲット配下か確認する
194+ *
195+ * @param string $entry
196+ * @param string $targetPath
197+ * @return bool
198+ * @checked
199+ * @noTodo
200+ * @unitTest
201+ */
202+ protected function _isZipEntrySafe ($ entry , $ targetPath )
203+ {
204+ $ entry = str_replace ('\\' , '/ ' , $ entry );
205+ if ($ entry === '' || strpos ($ entry , "\0" ) !== false ) {
206+ return false ;
207+ }
208+ if (preg_match ('/^[A-Za-z]:\// ' , $ entry ) || strpos ($ entry , '/ ' ) === 0 ) {
209+ return false ;
210+ }
211+ $ normalizedEntry = $ this ->_normalizeRelativePath ($ entry );
212+ // "." や "a/.." 等で正規化後に空になるエントリは展開対象として無意味なため拒否する。
213+ if ($ normalizedEntry === null || $ normalizedEntry === '' ) {
214+ return false ;
215+ }
216+ $ destPath = $ this ->_normalizeAbsolutePath ($ targetPath . '/ ' . $ normalizedEntry );
217+ if ($ destPath === null || $ destPath === '' ) {
218+ return false ;
219+ }
220+ $ comparisonDest = $ destPath . '/ ' ;
221+ $ comparisonTarget = $ targetPath . '/ ' ;
222+ if (DIRECTORY_SEPARATOR === '\\' ) {
223+ // Windows 互換性のため、大文字小文字を無視して比較する。
224+ $ comparisonDest = strtolower ($ comparisonDest );
225+ $ comparisonTarget = strtolower ($ comparisonTarget );
226+ }
227+ // 展開先がターゲット配下に収まることを保証する。
228+ return (strpos ($ comparisonDest , $ comparisonTarget ) === 0 );
229+ }
230+
231+ /**
232+ * ZIPエントリ一覧を検証する
233+ *
234+ * @param string $targetPath
235+ * @return bool
236+ * @checked
237+ * @noTodo
238+ * @unitTest
239+ */
240+ protected function _validateZipEntries ($ targetPath )
241+ {
242+ for ($ i = 0 ; $ i < $ this ->Zip ->numFiles ; $ i ++) {
243+ $ entry = $ this ->Zip ->getNameIndex ($ i );
244+ if ($ entry === false || !$ this ->_isZipEntrySafe ($ entry , $ targetPath )) {
245+ $ this ->error = __d ('baser_core ' , 'Invalid zip entry path: {0} ' , $ entry );
246+ return false ;
247+ }
248+ }
249+ return true ;
250+ }
251+
252+ /**
253+ * 展開先ディレクトリの正規化
254+ *
255+ * 展開前検証用に、存在しない場合は親ディレクトリで解決して基準パスを作る。
256+ *
257+ * @param string $target
258+ * @return string|false
259+ * @checked
260+ * @noTodo
261+ * @unitTest
262+ */
263+ protected function _normalizeTargetPath ($ target )
264+ {
265+ $ trimmedTarget = rtrim ($ target , "/ \\" );
266+ if ($ trimmedTarget === '' ) {
267+ $ this ->error = __d ('baser_core ' , 'Target directory not found. ' );
268+ return false ;
269+ }
270+ $ targetPath = realpath ($ trimmedTarget );
271+ if ($ targetPath === false ) {
272+ // ディレクトリ作成は行わず、比較用の基準パスのみ構成する。
273+ $ parentPath = realpath (dirname ($ trimmedTarget ));
274+ if ($ parentPath === false || !is_dir ($ parentPath )) {
275+ $ this ->error = __d ('baser_core ' , 'Target directory not found. ' );
276+ return false ;
277+ }
278+ $ targetPath = $ parentPath . '/ ' . basename ($ trimmedTarget );
279+ }
280+ return rtrim (str_replace ('\\' , '/ ' , $ targetPath ), '/ ' );
281+ }
282+
283+ /**
284+ * 相対パスを正規化する
285+ *
286+ * @param string $path
287+ * @return string|null
288+ * @checked
289+ * @noTodo
290+ * @unitTest
291+ */
292+ protected function _normalizeRelativePath ($ path )
293+ {
294+ $ path = str_replace ('\\' , '/ ' , $ path );
295+ $ parts = [];
296+ foreach (explode ('/ ' , $ path ) as $ part ) {
297+ if ($ part === '' || $ part === '. ' ) {
298+ continue ;
299+ }
300+ if ($ part === '.. ' ) {
301+ if (empty ($ parts )) {
302+ return null ;
303+ }
304+ array_pop ($ parts );
305+ continue ;
306+ }
307+ $ parts [] = $ part ;
308+ }
309+ return implode ('/ ' , $ parts );
310+ }
311+
312+ /**
313+ * 絶対パスを正規化する
314+ *
315+ * @param string $path
316+ * @return string|null
317+ * @checked
318+ * @noTodo
319+ * @unitTest
320+ */
321+ protected function _normalizeAbsolutePath ($ path )
322+ {
323+ // ZIP 内のエントリはまだ実在しないため、realpath() は使えない。
324+ $ path = str_replace ('\\' , '/ ' , $ path );
325+ $ drive = '' ;
326+ if (preg_match ('/^[A-Za-z]:/ ' , $ path )) {
327+ $ drive = strtoupper ($ path [0 ]) . ': ' ;
328+ $ path = substr ($ path , 2 );
329+ }
330+ $ parts = [];
331+ foreach (explode ('/ ' , $ path ) as $ part ) {
332+ if ($ part === '' || $ part === '. ' ) {
333+ continue ;
334+ }
335+ if ($ part === '.. ' ) {
336+ if (!empty ($ parts )) {
337+ array_pop ($ parts );
338+ } else {
339+ return null ;
340+ }
341+ continue ;
342+ }
343+ $ parts [] = $ part ;
344+ }
345+ $ normalized = implode ('/ ' , $ parts );
346+ if ($ drive !== '' ) {
347+ return $ drive . '/ ' . $ normalized ;
348+ }
349+ return '/ ' . $ normalized ;
350+ }
351+
161352 /**
162353 * zip生成
163354 *
0 commit comments