log_location_url = "{$uploads_dir['baseurl']}/{$this->log_file_dir}/{$log_file_name}"; $this->log_location_dir = "{$uploads_dir['basedir']}/{$this->log_file_dir}"; $this->log_location_file = "{$this->log_location_dir}/{$log_file_name}"; $this->log_index_file = "{$this->log_location_dir}/index.php"; define('WPFNL_LOG_FILE_DIR', $this->log_location_dir ); define('WPFNL_LOG_FILE', $this->log_location_file); } /** * Create the log folder. * * @since 2.5.9 */ public function create_log_folder() { wp_mkdir_p( $this->log_location_dir ); $this->protect_log_folder(); } /** * Harden the log directory. * * Server config files cannot carry this on their own: nginx never reads * .htaccess, and neither does Apache with AllowOverride None, so on those * hosts a log file under uploads stays fetchable no matter what is written * beside it. The unguessable file name suffix is therefore the defence that * holds everywhere, and the rules written here are the second layer for * servers that do honour them. * * The legacy directory used up to 3.0.5 is hardened too - installs that * upgraded through that release still have readable logs sitting there. * * @since 3.12.8 */ protected function protect_log_folder() { // Runs on every page load, so the settled state must cost nothing beyond // one autoloaded option read. if ( get_option( self::PROTECTION_OPTION ) === self::PROTECTION_VERSION ) { return; } $uploads = wp_upload_dir( null, false ); $legacy = ! empty( $uploads['basedir'] ) ? $uploads['basedir'] . '/wpfunnels-logs' : ''; foreach ( array( $this->log_location_dir, $legacy ) as $dir ) { if ( ! $dir || ! is_dir( $dir ) || ! is_writable( $dir ) ) { continue; } $this->write_protection_files( $dir, true ); $this->rename_predictable_logs( $dir ); } update_option( self::PROTECTION_OPTION, self::PROTECTION_VERSION ); } /** * Write the deny rules and placeholders into a log directory. * * @param String $dir Directory to protect. * @param Bool $refresh Rewrite files that already exist. * * @since 3.12.14 */ protected function write_protection_files( $dir, $refresh = false ) { $dir = trailingslashit( $dir ); // Order/Deny is Apache 2.2 syntax. On a 2.4 build without // mod_access_compat it is an unknown directive, which fails the whole // directory with a 500 instead of denying anything - hence the guards. $htaccess = $dir . '.htaccess'; if ( $refresh || ! file_exists( $htaccess ) ) { $rules = "# WPFunnels log files. Deny direct web access.\n"; $rules .= "\n"; $rules .= "\tRequire all denied\n"; $rules .= "\n"; $rules .= "\n"; $rules .= "\tOrder deny,allow\n"; $rules .= "\tDeny from all\n"; $rules .= "\n"; $rules .= "Options -Indexes\n"; @file_put_contents( $htaccess, $rules ); // @codingStandardsIgnoreLine. } $index = $dir . 'index.php'; if ( ! file_exists( $index ) ) { @file_put_contents( $index, "\n"; $config .= "\n"; $config .= "\t\n"; $config .= "\t\t\n"; $config .= "\t\t\t\n"; $config .= "\t\t\t\t\n"; $config .= "\t\t\t\t\t\n"; $config .= "\t\t\t\t\n"; $config .= "\t\t\t\n"; $config .= "\t\t\n"; $config .= "\t\n"; $config .= "\n"; @file_put_contents( $web_config, $config ); // @codingStandardsIgnoreLine. } } /** * Rename log files still carrying a guessable name. * * Files written before 3.12.14 are named from a fixed prefix and the date * alone, so hardening new writes is not enough - the existing files stay * fetchable. Renaming keeps the contents and closes the exposure. * * @param String $dir Directory to migrate. * * @since 3.12.14 */ protected function rename_predictable_logs( $dir ) { $suffix = self::get_log_file_suffix(); $files = glob( trailingslashit( $dir ) . '*.log' ); if ( ! is_array( $files ) ) { return; } foreach ( $files as $file ) { if ( false !== strpos( basename( $file ), $suffix ) ) { continue; } $target = substr( $file, 0, -4 ) . '-' . $suffix . '.log'; if ( ! file_exists( $target ) ) { @rename( $file, $target ); // @codingStandardsIgnoreLine. } } } /** * Site-specific suffix for log file names. * * Derived from the site salts, so it is stable for a site and unguessable * from outside it. This is what keeps the logs private on servers that * ignore .htaccess. * * @return String * @since 3.12.14 */ public static function get_log_file_suffix() { return wp_hash( 'wpfnl-log-file-name' ); } /** * Create log file. * * @param String $log_location_dir * @param String $file_name * * @since 2.5.9 * @return void */ public static function create_log_file( $log_location_dir, $file_name ) { if ( ! is_writable( $log_location_dir ) ) { return; } if ( file_exists( $file_name ) ) { return; } touch( $file_name ); } /** * Retrieve logging file location. * * @return string Logging file location. * * @since 2.5.9 */ public function get_logging_location() { return $this->log_location_file; } /** * Initialize Logging directory * * @since 2.5.9 */ public function initialize_logging() { $this->create_log_folder(); } /** * Update log file * * @param String $log_type * @param Mix $content * @param String $header_text * * @since 2.5.9 */ public static function modify_log_file( $log_type, $content, $header_text = '' ){ if( $log_type && $content && defined( 'WPFNL_LOG_FILE' ) && defined( 'WPFNL_LOG_FILE_DIR' ) ){ $file_name = WPFNL_LOG_FILE.'-'.trim(strtolower($log_type)).'-log-'.date('Y-m-d').'-'.self::get_log_file_suffix().'.log'; $time = new \DateTimeImmutable('now', wp_timezone()); $current_time = $time->format("h:i A"); $header_content = "\nWPFunnels - ".$current_time." - ".$header_text."\n"; if ( !file_exists( $file_name ) ) { self::create_log_file( WPFNL_LOG_FILE_DIR, $file_name ); } if ( ! is_writable( $file_name ) ) { return; } // Neutralise any PHP open tags in attacker-controlled content so the // log file can never be executed if it is ever included by PHP. $header_content = self::sanitize_log_content( $header_content ); $content = is_scalar( $content ) ? self::sanitize_log_content( (string) $content ) : ''; file_put_contents ($file_name, $header_content, FILE_APPEND ); file_put_contents ($file_name, $content, FILE_APPEND ); } } /** * Strip PHP open tags from log content to prevent code execution. * * @param String $content * * @return String * @since 3.12.8 */ protected static function sanitize_log_content( $content ) { return str_ireplace( array( ' $value ) { if ( ! in_array( $value, array( '.', '..' ), true ) ) { if ( ! is_dir( $value ) && strstr( $value, '.log' ) ) { $result[ sanitize_title( $value ) ] = $value; } } } } return $result; } }