PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.3
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.3
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
← All changes | includes/core/logger/class-wpfnl-logger.php +140 -8 3.12.11 → 3.13.3 View file →
@@ -9,10 +9,25 @@
9 9 class Wpfnl_Logger {
10 10
11 11 use SingletonTrait;
12 12
13 + /**
14 + * Bumped whenever the directory hardening below changes, so an install
15 + * hardened by an earlier release re-runs it once on upgrade.
16 + *
17 + * @since 3.12.14
18 + */
19 + const PROTECTION_VERSION = '2';
13 20
21 + /**
22 + * Option holding the hardening version already applied.
23 + *
24 + * @since 3.12.14
25 + */
26 + const PROTECTION_OPTION = '_wpfnl_log_protection_version';
14 27
28 +
29 +
15 30 /**
16 31 * Log location, URL path.
17 32 *
18 33 * @since 2.5.9
@@ -84,30 +99,147 @@
84 99 }
85 100
86 101
87 102 /**
88 - * Harden the log directory: deny direct web access and prevent directory listing.
103 + * Harden the log directory.
89 104 *
105 + * Server config files cannot carry this on their own: nginx never reads
106 + * .htaccess, and neither does Apache with AllowOverride None, so on those
107 + * hosts a log file under uploads stays fetchable no matter what is written
108 + * beside it. The unguessable file name suffix is therefore the defence that
109 + * holds everywhere, and the rules written here are the second layer for
110 + * servers that do honour them.
111 + *
112 + * The legacy directory used up to 3.0.5 is hardened too - installs that
113 + * upgraded through that release still have readable logs sitting there.
114 + *
90 115 * @since 3.12.8
91 116 */
92 117 protected function protect_log_folder() {
93 - if ( ! is_dir( $this->log_location_dir ) || ! is_writable( $this->log_location_dir ) ) {
118 + // Runs on every page load, so the settled state must cost nothing beyond
119 + // one autoloaded option read.
120 + if ( get_option( self::PROTECTION_OPTION ) === self::PROTECTION_VERSION ) {
94 121 return;
95 122 }
96 123
97 - $htaccess = trailingslashit( $this->log_location_dir ) . '.htaccess';
98 - if ( ! file_exists( $htaccess ) ) {
99 - file_put_contents( $htaccess, "Order deny,allow\nDeny from all\n" );
124 + $uploads = wp_upload_dir( null, false );
125 + $legacy = ! empty( $uploads['basedir'] ) ? $uploads['basedir'] . '/wpfunnels-logs' : '';
126 +
127 + foreach ( array( $this->log_location_dir, $legacy ) as $dir ) {
128 + if ( ! $dir || ! is_dir( $dir ) || ! is_writable( $dir ) ) {
129 + continue;
130 + }
131 +
132 + $this->write_protection_files( $dir, true );
133 + $this->rename_predictable_logs( $dir );
100 134 }
101 135
102 - $index = trailingslashit( $this->log_location_dir ) . 'index.php';
136 + update_option( self::PROTECTION_OPTION, self::PROTECTION_VERSION );
137 + }
138 +
139 +
140 + /**
141 + * Write the deny rules and placeholders into a log directory.
142 + *
143 + * @param String $dir Directory to protect.
144 + * @param Bool $refresh Rewrite files that already exist.
145 + *
146 + * @since 3.12.14
147 + */
148 + protected function write_protection_files( $dir, $refresh = false ) {
149 + $dir = trailingslashit( $dir );
150 +
151 + // Order/Deny is Apache 2.2 syntax. On a 2.4 build without
152 + // mod_access_compat it is an unknown directive, which fails the whole
153 + // directory with a 500 instead of denying anything - hence the guards.
154 + $htaccess = $dir . '.htaccess';
155 + if ( $refresh || ! file_exists( $htaccess ) ) {
156 + $rules = "# WPFunnels log files. Deny direct web access.\n";
157 + $rules .= "<IfModule mod_authz_core.c>\n";
158 + $rules .= "\tRequire all denied\n";
159 + $rules .= "</IfModule>\n";
160 + $rules .= "<IfModule !mod_authz_core.c>\n";
161 + $rules .= "\tOrder deny,allow\n";
162 + $rules .= "\tDeny from all\n";
163 + $rules .= "</IfModule>\n";
164 + $rules .= "Options -Indexes\n";
165 +
166 + @file_put_contents( $htaccess, $rules ); // @codingStandardsIgnoreLine.
167 + }
168 +
169 + $index = $dir . 'index.php';
103 170 if ( ! file_exists( $index ) ) {
104 - file_put_contents( $index, "<?php\n// Silence is golden.\n" );
171 + @file_put_contents( $index, "<?php\n// Silence is golden.\n" ); // @codingStandardsIgnoreLine.
105 172 }
173 +
174 + $web_config = $dir . 'web.config';
175 + if ( $refresh || ! file_exists( $web_config ) ) {
176 + $config = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
177 + $config .= "<configuration>\n";
178 + $config .= "\t<system.webServer>\n";
179 + $config .= "\t\t<security>\n";
180 + $config .= "\t\t\t<requestFiltering>\n";
181 + $config .= "\t\t\t\t<fileExtensions>\n";
182 + $config .= "\t\t\t\t\t<add fileExtension=\".log\" allowed=\"false\" />\n";
183 + $config .= "\t\t\t\t</fileExtensions>\n";
184 + $config .= "\t\t\t</requestFiltering>\n";
185 + $config .= "\t\t</security>\n";
186 + $config .= "\t</system.webServer>\n";
187 + $config .= "</configuration>\n";
188 +
189 + @file_put_contents( $web_config, $config ); // @codingStandardsIgnoreLine.
190 + }
106 191 }
107 192
108 193
109 194 /**
195 + * Rename log files still carrying a guessable name.
196 + *
197 + * Files written before 3.12.14 are named from a fixed prefix and the date
198 + * alone, so hardening new writes is not enough - the existing files stay
199 + * fetchable. Renaming keeps the contents and closes the exposure.
200 + *
201 + * @param String $dir Directory to migrate.
202 + *
203 + * @since 3.12.14
204 + */
205 + protected function rename_predictable_logs( $dir ) {
206 + $suffix = self::get_log_file_suffix();
207 + $files = glob( trailingslashit( $dir ) . '*.log' );
208 +
209 + if ( ! is_array( $files ) ) {
210 + return;
211 + }
212 +
213 + foreach ( $files as $file ) {
214 + if ( false !== strpos( basename( $file ), $suffix ) ) {
215 + continue;
216 + }
217 +
218 + $target = substr( $file, 0, -4 ) . '-' . $suffix . '.log';
219 + if ( ! file_exists( $target ) ) {
220 + @rename( $file, $target ); // @codingStandardsIgnoreLine.
221 + }
222 + }
223 + }
224 +
225 +
226 + /**
227 + * Site-specific suffix for log file names.
228 + *
229 + * Derived from the site salts, so it is stable for a site and unguessable
230 + * from outside it. This is what keeps the logs private on servers that
231 + * ignore .htaccess.
232 + *
233 + * @return String
234 + * @since 3.12.14
235 + */
236 + public static function get_log_file_suffix() {
237 + return wp_hash( 'wpfnl-log-file-name' );
238 + }
239 +
240 +
241 + /**
110 242 * Create log file.
111 243 *
112 244 * @param String $log_location_dir
113 245 * @param String $file_name
@@ -164,9 +296,9 @@
164 296 public static function modify_log_file( $log_type, $content, $header_text = '' ){
165 297
166 298 if( $log_type && $content && defined( 'WPFNL_LOG_FILE' ) && defined( 'WPFNL_LOG_FILE_DIR' ) ){
167 299
168 - $file_name = WPFNL_LOG_FILE.'-'.trim(strtolower($log_type)).'-log-'.date('Y-m-d').'.log';
300 + $file_name = WPFNL_LOG_FILE.'-'.trim(strtolower($log_type)).'-log-'.date('Y-m-d').'-'.self::get_log_file_suffix().'.log';
169 301 $time = new \DateTimeImmutable('now', wp_timezone());
170 302 $current_time = $time->format("h:i A");
171 303 $header_content = "\nWPFunnels - ".$current_time." - ".$header_text."\n";
172 304