| @@ -106,12 +106,12 @@ | ||
| 106 | 106 | if ( ! $this->is_wpconfig_writable() ) { |
| 107 | 107 | return new WP_Error( 'not_writable', __( 'wp-config.php is not writable', 'vigilante' ) ); |
| 108 | 108 | } |
| 109 | 109 | |
| 110 | - // Safety check 2: Create backup BEFORE any modification | |
| 111 | - $backup_result = $this->create_backup(); | |
| 112 | - if ( is_wp_error( $backup_result ) ) { | |
| 113 | - return $backup_result; | |
| 110 | + // Safety check 2: the file must look whole BEFORE any modification | |
| 111 | + $check_result = $this->check_before_write(); | |
| 112 | + if ( is_wp_error( $check_result ) ) { | |
| 113 | + return $check_result; | |
| 114 | 114 | } |
| 115 | 115 | |
| 116 | 116 | // First clean up old plugin constants |
| 117 | 117 | $this->remove_old_constants(); |
| @@ -132,8 +132,14 @@ | ||
| 132 | 132 | |
| 133 | 133 | // Regenerate critical file baseline so the integrity scan does not |
| 134 | 134 | // flag our own modifications as unauthorized changes. |
| 135 | 135 | if ( true === $result ) { |
| 136 | + // Record the block as Vigilant's own first: the baseline refreshed | |
| 137 | + // below leaves out of its hash only the blocks recorded this way. | |
| 138 | + if ( class_exists( 'Vigilante_File_Integrity' ) ) { | |
| 139 | + Vigilante_File_Integrity::remember_owned_block( 'wp-config.php', self::MARKER_START, rtrim( $constants, "\n" ) ); | |
| 140 | + } | |
| 141 | + | |
| 136 | 142 | /** |
| 137 | 143 | * Fires after Vigilante successfully writes to wp-config.php. |
| 138 | 144 | * Used by the file integrity module to update the baseline hash. |
| 139 | 145 | */ |
| @@ -143,13 +149,18 @@ | ||
| 143 | 149 | return $result; |
| 144 | 150 | } |
| 145 | 151 | |
| 146 | 152 | /** |
| 147 | - * Create a backup of wp-config.php before modification | |
| 153 | + * Check wp-config.php before modifying it | |
| 148 | 154 | * |
| 155 | + * Until 2.11.6 this also stored the whole file in vigilante_wpconfig_backup, | |
| 156 | + * and with it the database password and the authentication keys and salts. | |
| 157 | + * Nothing ever read that copy back to restore anything: the checks are what | |
| 158 | + * protected the file, and the copy only put its secrets in the options table. | |
| 159 | + * | |
| 149 | 160 | * @return bool|WP_Error |
| 150 | 161 | */ |
| 151 | - private function create_backup() { | |
| 162 | + private function check_before_write() { | |
| 152 | 163 | if ( ! file_exists( $this->wpconfig_path ) ) { |
| 153 | 164 | return new WP_Error( 'no_config', __( 'wp-config.php does not exist', 'vigilante' ) ); |
| 154 | 165 | } |
| 155 | 166 | |
| @@ -163,25 +174,10 @@ | ||
| 163 | 174 | if ( ! $this->validate_wpconfig_content( $content ) ) { |
| 164 | 175 | return new WP_Error( 'invalid_config', __( 'wp-config.php does not appear to be a valid WordPress configuration file', 'vigilante' ) ); |
| 165 | 176 | } |
| 166 | 177 | |
| 167 | - // Store the backup in a private database option, never as a file under | |
| 168 | - // the web root. wp-config.php holds DB credentials and salts; a file in | |
| 169 | - // wp-content could be served by a misconfigured server. The option is | |
| 170 | - // not reachable over HTTP and is not autoloaded. | |
| 171 | - $stored = update_option( | |
| 172 | - 'vigilante_wpconfig_backup', | |
| 173 | - array( | |
| 174 | - 'content' => $content, | |
| 175 | - 'time' => time(), | |
| 176 | - ), | |
| 177 | - false | |
| 178 | - ); | |
| 179 | - | |
| 180 | - // update_option() returns false both on failure and when the value is | |
| 181 | - // unchanged; only treat it as an error if the content was not stored. | |
| 182 | - if ( false === $stored && $content !== $this->get_wpconfig_backup_content() ) { | |
| 183 | - return new WP_Error( 'backup_failed', __( 'Could not create wp-config.php backup', 'vigilante' ) ); | |
| 178 | + if ( ! self::constants_blocks_are_whole( $content ) ) { | |
| 179 | + return new WP_Error( 'block_incomplete', __( 'A Vigilant block in wp-config.php is missing one of its markers, so the file was left as it is.', 'vigilante' ) ); | |
| 184 | 180 | } |
| 185 | 181 | |
| 186 | 182 | return true; |
| 187 | 183 | } |
| @@ -186,15 +182,43 @@ | ||
| 186 | 182 | return true; |
| 187 | 183 | } |
| 188 | 184 | |
| 189 | 185 | /** |
| 190 | - * Get the stored wp-config.php backup content, or '' if none. | |
| 186 | + * Whether no constants block in wp-config.php is left without its END | |
| 191 | 187 | * |
| 192 | - * @return string | |
| 188 | + * A BEGIN with no END after it, or a second BEGIN before the END, means the | |
| 189 | + * block cannot be found whole. Removing it would still rewrite the file and | |
| 190 | + * uncomment the original constants around it while the block keeps defining | |
| 191 | + * them, and writing a new one would leave the broken block in place for the | |
| 192 | + * next removal to pair with the new END, taking everything in between. So | |
| 193 | + * the file is left as it is, and the block keeps working until someone | |
| 194 | + * removes it by hand. An END with no BEGIN before it is left out of the | |
| 195 | + * question: each BEGIN is paired with the next END and a stray END is left | |
| 196 | + * alone. | |
| 197 | + * | |
| 198 | + * Until 2.11.6 only deactivation asked this, from its own copy of the check. | |
| 199 | + * | |
| 200 | + * @since 2.11.6 | |
| 201 | + * | |
| 202 | + * @param string $content wp-config.php content. | |
| 203 | + * @return bool | |
| 193 | 204 | */ |
| 194 | - private function get_wpconfig_backup_content() { | |
| 195 | - $backup = get_option( 'vigilante_wpconfig_backup' ); | |
| 196 | - return ( is_array( $backup ) && isset( $backup['content'] ) ) ? (string) $backup['content'] : ''; | |
| 205 | + private static function constants_blocks_are_whole( $content ) { | |
| 206 | + preg_match_all( '/' . preg_quote( self::MARKER_START, '/' ) . '|' . preg_quote( self::MARKER_END, '/' ) . '/', $content, $markers ); | |
| 207 | + | |
| 208 | + $inside = false; | |
| 209 | + foreach ( $markers[0] as $marker ) { | |
| 210 | + if ( self::MARKER_START === $marker ) { | |
| 211 | + if ( $inside ) { | |
| 212 | + return false; | |
| 213 | + } | |
| 214 | + $inside = true; | |
| 215 | + } else { | |
| 216 | + $inside = false; | |
| 217 | + } | |
| 218 | + } | |
| 219 | + | |
| 220 | + return ! $inside; | |
| 197 | 221 | } |
| 198 | 222 | |
| 199 | 223 | /** |
| 200 | 224 | * Read file directly without WP_Filesystem (more reliable) |
| @@ -366,8 +390,19 @@ | ||
| 366 | 390 | return true; |
| 367 | 391 | } |
| 368 | 392 | |
| 369 | 393 | /** |
| 394 | + * Drop the integrity scan's record of the constants block once it is gone | |
| 395 | + * | |
| 396 | + * @since 2.11.5 | |
| 397 | + */ | |
| 398 | + private function forget_owned_block() { | |
| 399 | + if ( class_exists( 'Vigilante_File_Integrity' ) ) { | |
| 400 | + Vigilante_File_Integrity::forget_owned_blocks( 'wp-config.php', self::MARKER_START ); | |
| 401 | + } | |
| 402 | + } | |
| 403 | + | |
| 404 | + /** | |
| 370 | 405 | * Remove our security constants from wp-config.php and restore originals |
| 371 | 406 | * |
| 372 | 407 | * @return bool|WP_Error |
| 373 | 408 | */ |
| @@ -387,10 +422,17 @@ | ||
| 387 | 422 | if ( false === $content ) { |
| 388 | 423 | return false; |
| 389 | 424 | } |
| 390 | 425 | |
| 426 | + // A block that lost a marker cannot come out without cutting or | |
| 427 | + // duplicating what surrounds it, so the file is left as it is. | |
| 428 | + if ( ! self::constants_blocks_are_whole( $content ) ) { | |
| 429 | + return new WP_Error( 'block_incomplete', __( 'A Vigilant block in wp-config.php is missing one of its markers, so the file was left as it is.', 'vigilante' ) ); | |
| 430 | + } | |
| 431 | + | |
| 391 | 432 | // If our markers don't exist, just try to uncomment originals |
| 392 | 433 | if ( strpos( $content, self::MARKER_START ) === false ) { |
| 434 | + $this->forget_owned_block(); | |
| 393 | 435 | return $this->uncomment_original_constants(); |
| 394 | 436 | } |
| 395 | 437 | |
| 396 | 438 | // Validate before modification |
| @@ -414,8 +456,10 @@ | ||
| 414 | 456 | // Write the file without our block |
| 415 | 457 | if ( ! $this->write_file_directly( $this->wpconfig_path, $new_content ) ) { |
| 416 | 458 | return false; |
| 417 | 459 | } |
| 460 | + | |
| 461 | + $this->forget_owned_block(); | |
| 418 | 462 | |
| 419 | 463 | // Now uncomment the original constants |
| 420 | 464 | return $this->uncomment_original_constants(); |
| 421 | 465 | } |