| @@ -652,11 +652,14 @@ | ||
| 652 | 652 | ob_start(); |
| 653 | 653 | |
| 654 | 654 | $output = fopen( 'php://output', 'w' ); |
| 655 | 655 | |
| 656 | - // Add CSV rows | |
| 656 | + // Add CSV rows. Neutralize spreadsheet formula injection: a cell that a | |
| 657 | + // lower-privileged author controls (e.g. a doc/FAQ title or term name) could | |
| 658 | + // start with =, +, -, @, or a tab/CR and execute when the admin opens the | |
| 659 | + // export in Excel/LibreOffice. Prefix such cells with a single quote. | |
| 657 | 660 | foreach ( $data as $row ) { |
| 658 | - fputcsv( $output, $row ); | |
| 661 | + fputcsv( $output, array_map( [ $this, 'neutralize_csv_cell' ], (array) $row ) ); | |
| 659 | 662 | } |
| 660 | 663 | |
| 661 | 664 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- closing php://output stream; WP_Filesystem does not apply. |
| 662 | 665 | fclose( $output ); |
| @@ -661,6 +664,20 @@ | ||
| 661 | 664 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- closing php://output stream; WP_Filesystem does not apply. |
| 662 | 665 | fclose( $output ); |
| 663 | 666 | |
| 664 | 667 | return ob_get_clean(); |
| 668 | + } | |
| 669 | + | |
| 670 | + /** | |
| 671 | + * Prefix a leading formula trigger (= + - @ tab CR) with a single quote so | |
| 672 | + * spreadsheet apps treat the cell as text instead of executing it. | |
| 673 | + */ | |
| 674 | + private function neutralize_csv_cell( $cell ) { | |
| 675 | + $cell = (string) $cell; | |
| 676 | + | |
| 677 | + if ( $cell !== '' && preg_match( '/^[=+\-@\t\r]/', $cell ) ) { | |
| 678 | + return "'" . $cell; | |
| 679 | + } | |
| 680 | + | |
| 681 | + return $cell; | |
| 665 | 682 | } |
| 666 | 683 | } |