`. Under php-fpm * behind `mod_proxy_fcgi` — the common modern setup — no PHP module is * loaded, so both blocks were skipped and nothing turned the interpreter off. * `RemoveHandler` covers the `AddHandler` mappings a host may have set. * * It does **not** undo a vhost-level `SetHandler "proxy:fcgi://..."`, which is * what php-fpm normally uses, and a `.htaccess` cannot. The protection that * actually holds under php-fpm is the `Require all denied` clause below, * which refuses the request before any handler is consulted. * - The pattern missed `pht`, `phtm`, `htm`, `html`, `xhtml`, `svg`, `svgz`, * `jsp` and `user.ini`, and `php[0-9]` stops matching at `php10`. * - `Options -Indexes` only covers Apache, so an `index.php` is still written. */ private static function htaccess(): string { $deny = array( "\t", "\t\tRequire all denied", "\t", "\t", "\t\tOrder allow,deny", "\t\tDeny from all", "\t", ); return implode( "\n", array_merge( array( '# Added by FormPilot — Smart Form Builder. Uploads are data, never code.', '', ), $deny, array( '', '', '', ), $deny, array( '', '', '# Works whether PHP runs as a module, via CGI, or over FastCGI. Wrapped, because', '# RemoveHandler and RemoveType need AllowOverride FileInfo: unguarded, a host', '# that grants only Limit answers 500 for this whole directory, which would make', '# every already-uploaded file undownloadable.', '', "\tRemoveHandler .php .php3 .php4 .php5 .php7 .php8 .phtml .pht .phar", "\tRemoveType .php .php3 .php4 .php5 .php7 .php8 .phtml .pht .phar", '', '', '', "\tphp_flag engine off", '', '', '', "\tphp_flag engine off", '', '', 'Options -Indexes', '', ) ) ); } /** * Returns the IIS equivalent of the execution rules. * * `OrphanCleaner::PROTECTED_FILES` has always listed `web.config`, so one was * clearly intended; none was ever written. */ private static function web_config(): string { $blocked = array(); foreach ( array( '.php', '.phtml', '.phar', '.asp', '.aspx', '.svg', '.html', '.htm' ) as $extension ) { $blocked[] = "\t\t\t\t\t" . ''; } return implode( "\n", array_merge( array( '', '', '', "\t", "\t\t" . '', "\t\t", "\t\t\t", "\t\t\t\t" . '', ), $blocked, array( "\t\t\t\t", "\t\t\t", "\t\t", "\t\t" . '', "\t", '', '', ) ) ); } /** * Writes a protective file when it is missing or out of date. * * Reports what happened. The original returned void and bailed silently when * `$wp_filesystem` was unavailable, and its one caller discarded the answer — * so uploads proceeded into a directory that had never been hardened, with * nothing anywhere recording that it had not been. * * @param string $path Absolute file path. * @param string $contents Desired content. * * @return bool True when the file is present with the expected content. */ private static function write( string $path, string $contents ): bool { global $wp_filesystem; if ( is_file( $path ) && file_get_contents( $path ) === $contents ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Comparing a file the plugin wrote itself. return true; } if ( ! function_exists( 'WP_Filesystem' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } WP_Filesystem(); if ( $wp_filesystem instanceof \WP_Filesystem_Base ) { return (bool) $wp_filesystem->put_contents( $path, $contents, FS_CHMOD_FILE ); } /* * No abstraction available — `FS_METHOD` is set to FTP or SSH and there are * no credentials in a front-end request. Writing directly is correct here: * the alternative is an unprotected directory. * * Through core's direct transport rather than raw PHP calls, which is what * the branch above already is on most hosts. Every writer puts the same * bytes at the same offsets, so two concurrent first uploads cannot leave an * interleaved .htaccess behind. */ if ( ! class_exists( 'WP_Filesystem_Direct' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; } return ( new \WP_Filesystem_Direct( null ) )->put_contents( $path, $contents, self::file_mode() ); } /** * Returns the permissions a guard file is written with. * * `WP_Filesystem()` only defines `FS_CHMOD_FILE` after it has connected, and the * fallback above runs precisely when it could not. Reading the constant there * was an `Undefined constant` fatal on the first upload to an FTP-configured * site, so the value is worked out here the way core works it out. */ private static function file_mode(): int { if ( defined( 'FS_CHMOD_FILE' ) ) { return (int) FS_CHMOD_FILE; } return fileperms( ABSPATH . 'index.php' ) & 0777 | 0644; } }