@@ -79,7 +79,7 @@ function extract_from_phar( $path ) {
7979
8080 $ fname = Path::basename ( $ path );
8181
82- $ tmp_path = get_temp_dir () . uniqid ( 'wp-cli-extract-from-phar- ' , true ) . "- $ fname " ;
82+ $ tmp_path = make_temp_file ( 'wp-cli-extract-from-phar- ' , "- $ fname " ) ;
8383
8484 copy ( $ path , $ tmp_path );
8585
@@ -599,28 +599,39 @@ function launch_editor_for_input( $input, $title = 'WP-CLI', $ext = 'tmp' ) {
599599
600600 $ tmpdir = get_temp_dir ();
601601
602+ $ created = false ;
603+ $ attempts = 0 ;
604+
602605 do {
603606 $ tmpfile = Path::basename ( $ title );
604607 $ tmpfile = preg_replace ( '|\.[^.]*$| ' , '' , $ tmpfile );
605608 $ tmpfile .= '- ' . substr ( md5 ( (string ) mt_rand () ), 0 , 6 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- no crypto and WP not loaded.
606609 $ tmpfile = $ tmpdir . $ tmpfile . '. ' . $ ext ;
607- $ fp = fopen ( $ tmpfile , 'xb ' );
608- if ( ! $ fp && is_writable ( $ tmpdir ) && file_exists ( $ tmpfile ) ) {
609- $ tmpfile = '' ;
610- continue ;
611- }
610+
611+ // Restrict the file to its owner, as it can hold sensitive input.
612+ $ old_umask = umask ( 0177 );
613+ $ fp = @fopen ( $ tmpfile , 'xb ' );
614+ umask ( $ old_umask );
615+
612616 if ( $ fp ) {
617+ $ created = true ;
618+ // A short write (e.g. on a full disk) must not leave a truncated file for the editor to open.
619+ if ( strlen ( $ input ) !== fwrite ( $ fp , $ input ) ) {
620+ fclose ( $ fp );
621+ @unlink ( $ tmpfile );
622+ WP_CLI ::error ( 'Error writing to temporary file. ' );
623+ }
613624 fclose ( $ fp );
625+ break ;
614626 }
615- } while ( ! $ tmpfile );
627+ $ tmpfile = '' ;
628+ ++$ attempts ;
629+ } while ( $ attempts < 100 );
616630
617- // @phpstan-ignore booleanNot.alwaysFalse
618- if ( ! $ tmpfile ) {
631+ if ( ! $ created ) {
619632 WP_CLI ::error ( 'Error creating temporary file. ' );
620633 }
621634
622- file_put_contents ( $ tmpfile , $ input );
623-
624635 $ editor = getenv ( 'EDITOR ' );
625636 if ( ! $ editor ) {
626637 $ editor = is_windows () ? 'notepad ' : 'vi ' ;
@@ -1337,6 +1348,90 @@ function get_temp_dir() {
13371348 return $ temp ;
13381349}
13391350
1351+ /**
1352+ * Create a unique temporary file safely without following symlinks.
1353+ *
1354+ * The file is only readable and writable by its owner on non-Windows systems.
1355+ *
1356+ * The file is created in the directory returned by WP-CLI's `get_temp_dir()`
1357+ * (based on `sys_get_temp_dir()`), not WordPress's `get_temp_dir()`, and is
1358+ * not removed automatically; callers are responsible for cleaning it up.
1359+ *
1360+ * On failure, this function exits via `WP_CLI::error()` rather than throwing.
1361+ *
1362+ * @access public
1363+ * @category System
1364+ *
1365+ * @param string $prefix Optional. Prefix for the temporary file name. Default 'wp-cli-'.
1366+ * @param string $suffix Optional. Suffix for the temporary file name. Default ''.
1367+ * @return string Path to the created temporary file.
1368+ */
1369+ function make_temp_file ( $ prefix = 'wp-cli- ' , $ suffix = '' ) {
1370+ if ( false !== strpbrk ( $ prefix , "/ \\\0" ) || false !== strpbrk ( $ suffix , "/ \\\0" ) ) {
1371+ WP_CLI ::error ( 'Invalid temporary file prefix or suffix. ' );
1372+ }
1373+
1374+ $ temp_dir = get_temp_dir ();
1375+ $ attempts = 0 ;
1376+
1377+ do {
1378+ $ path = $ temp_dir . uniqid ( $ prefix , true ) . $ suffix ;
1379+
1380+ // Restrict the file to its owner, as callers may write sensitive data to it.
1381+ $ old_umask = umask ( 0177 );
1382+ $ handle = @fopen ( $ path , 'xb ' );
1383+ umask ( $ old_umask );
1384+
1385+ ++$ attempts ;
1386+ } while ( ! $ handle && $ attempts < 100 );
1387+
1388+ if ( ! $ handle ) {
1389+ WP_CLI ::error ( 'Failed to create a temporary file. ' );
1390+ }
1391+
1392+ fclose ( $ handle );
1393+
1394+ return $ path ;
1395+ }
1396+
1397+ /**
1398+ * Create a unique temporary directory safely without following symlinks.
1399+ *
1400+ * The directory is only accessible by its owner on non-Windows systems.
1401+ *
1402+ * The directory is created in the directory returned by WP-CLI's `get_temp_dir()`
1403+ * (based on `sys_get_temp_dir()`), not WordPress's `get_temp_dir()`, and is
1404+ * not removed automatically; callers are responsible for cleaning it up.
1405+ *
1406+ * On failure, this function exits via `WP_CLI::error()` rather than throwing.
1407+ *
1408+ * @access public
1409+ * @category System
1410+ *
1411+ * @param string $prefix Optional. Prefix for the temporary directory name. Default 'wp-cli-'.
1412+ * @return string Path to the created temporary directory with a trailing slash.
1413+ */
1414+ function make_temp_dir ( $ prefix = 'wp-cli- ' ) {
1415+ if ( false !== strpbrk ( $ prefix , "/ \\\0" ) ) {
1416+ WP_CLI ::error ( 'Invalid temporary directory prefix. ' );
1417+ }
1418+
1419+ $ temp_dir = get_temp_dir ();
1420+ $ attempts = 0 ;
1421+
1422+ do {
1423+ $ path = $ temp_dir . uniqid ( $ prefix , true );
1424+ $ success = @mkdir ( $ path , 0700 );
1425+ ++$ attempts ;
1426+ } while ( ! $ success && $ attempts < 100 );
1427+
1428+ if ( ! $ success ) {
1429+ WP_CLI ::error ( 'Failed to create a temporary directory. ' );
1430+ }
1431+
1432+ return Path::trailingslashit ( $ path );
1433+ }
1434+
13401435/**
13411436 * Parse a SSH url for its host, port, and path.
13421437 *
0 commit comments