| @@ -97,65 +97,39 @@ | ||
| 97 | 97 | $key = $this->get_key( $key ); |
| 98 | 98 | $wp_session[ $key ] = $value; |
| 99 | 99 | } |
| 100 | 100 | |
| 101 | - // Request-scoped state. | |
| 102 | - // | |
| 103 | - // These values are the plugin's own per-request scratch space: the post being | |
| 104 | - // rendered, the ids of snippets already written, the pending admin notices. | |
| 105 | - // Every one of them is produced by this plugin during the request and read | |
| 106 | - // back by it later in the same request. None is ever legitimately supplied by | |
| 107 | - // the client. | |
| 108 | - // | |
| 109 | - // getRequest() used to fall back to $_POST['TCM_' . $key] whenever a key was | |
| 110 | - // absent from the in-memory store, which turned all of that internal state | |
| 111 | - // into an unauthenticated input surface. That is CVE-2026-15180: a POST of | |
| 112 | - // TCM_ErrorMessages[0] landed in the admin notice box on the next page render. | |
| 113 | - // The fallback was already vestigial — nothing in the plugin has written to | |
| 114 | - // $_POST['TCM_*'] since 2.5.0 (RDU-1630) — so it is removed outright rather | |
| 115 | - // than gated behind a nonce, which would have kept an attacker-influenced | |
| 116 | - // path alive for no caller's benefit. | |
| 117 | - // | |
| 118 | - // Consequence for removeRequest(): unsetting the POST field is no longer a | |
| 119 | - // way to clear a key, so it now clears the in-memory entry. That also fixes | |
| 120 | - // the clean-up it was always meant to perform — writeMessages( clean: true ) | |
| 121 | - // previously only ever removed a POST field that the plugin itself never set, | |
| 122 | - // so an internally queued notice survived its own render and could be emitted | |
| 123 | - // a second time by the next writeMessages() call in the same request | |
| 124 | - // (includes/admin/editor.php does exactly that). | |
| 101 | + //$_REQUEST | |
| 102 | + //However WP enforces its own logic - during load process wp_magic_quotes() processes variables to emulate magic quotes setting and enforces $_REQUEST to contain combination of $_GET and $_POST, no matter what PHP configuration says. | |
| 125 | 103 | private function removeRequest( $key ) { |
| 126 | - unset( $this->request_data[ $key ] ); | |
| 104 | + $key = $this->get_key( $key ); | |
| 105 | + if ( isset( $_POST[ $key ] ) ) { | |
| 106 | + unset( $_POST[ $key ] ); | |
| 107 | + } | |
| 127 | 108 | } |
| 128 | 109 | private function getRequest( $key, $default = false ) { |
| 129 | 110 | $result = $default; |
| 130 | - if ( isset( $this->request_data[ $key ] ) ) { | |
| 131 | - if ( is_object( $this->request_data[ $key ] ) ) { | |
| 132 | - $result = clone $this->request_data[ $key ]; | |
| 133 | - } else { | |
| 134 | - $result = $this->request_data[ $key ]; | |
| 111 | + if ( isset($this->request_data[ $key ]) ) { | |
| 112 | + if ( is_object( $this->request_data[ $key ] ) ) { | |
| 113 | + $result = clone $this->request_data[ $key ]; | |
| 114 | + } else { | |
| 115 | + $result = $this->request_data[ $key ]; | |
| 116 | + } | |
| 117 | + $result = $this->recursive_wp_kses( $result ); | |
| 118 | + } else { | |
| 119 | + $key = $this->get_key( $key ); | |
| 120 | + if ( isset( $_POST[ $key ] ) ) { | |
| 121 | + if ( is_object( $_POST[ $key ] ) ) { | |
| 122 | + $result = clone $_POST[ $key ]; | |
| 123 | + } else { | |
| 124 | + $result = $_POST[ $key ]; | |
| 125 | + } | |
| 126 | + $result = $this->recursive_wp_kses( $result ); | |
| 135 | 127 | } |
| 136 | - $result = $this->recursive_wp_kses( $result ); | |
| 137 | 128 | } |
| 138 | 129 | return $result; |
| 139 | 130 | } |
| 140 | 131 | |
| 141 | - // Sanitize a request-scoped value tree. | |
| 142 | - // | |
| 143 | - // The permissive whitelist in tcmp_free_wp_kses_tags_attrs.php exists for | |
| 144 | - // exactly one kind of value: a snippet body. Those are admin-authored | |
| 145 | - // tracking snippets and legitimately contain <script>, <iframe> and onload — | |
| 146 | - // that is the plugin's purpose. Applying the same whitelist to every other | |
| 147 | - // string, as this method used to, extended "may contain executable markup" to | |
| 148 | - // values that are never tracking code: notice text, names, ids. | |
| 149 | - // | |
| 150 | - // So the 'code' field keeps the permissive pass (still opt-out-able via Skip | |
| 151 | - // Code Sanitization, since a snippet may need markup the whitelist omits) and | |
| 152 | - // everything else gets wp_kses_post(), which keeps the formatting notices | |
| 153 | - // legitimately embed — <a href>, <strong>, <span style> — while dropping | |
| 154 | - // script/iframe/style and every on* handler. | |
| 155 | - // | |
| 156 | - // This mirrors the split TCMP_Utils::sanitize_post_or_get() already applies on | |
| 157 | - // the save path (F-06): permissive for 'code', plain text for the rest. | |
| 158 | 132 | public function recursive_wp_kses( $array ) { |
| 159 | 133 | global $tcmp_allowed_html_tags; |
| 160 | 134 | foreach ( $array as $key => &$value ) { |
| 161 | 135 | if ( is_array( $value ) ) { |
| @@ -164,9 +138,9 @@ | ||
| 164 | 138 | if ( ! $this->getSkipCodeSanitization() ) { |
| 165 | 139 | $value = wp_kses( $value, $tcmp_allowed_html_tags ); |
| 166 | 140 | } |
| 167 | 141 | } elseif ( is_string( $value ) ) { |
| 168 | - $value = wp_kses_post( $value ); | |
| 142 | + $value = wp_kses( $value, $tcmp_allowed_html_tags ); | |
| 169 | 143 | } else { |
| 170 | 144 | // do nothing ... could be a video or graphics object |
| 171 | 145 | } |
| 172 | 146 | } |
| @@ -388,19 +362,14 @@ | ||
| 388 | 362 | private function writeGenericMessages( $type, $clean = true ) { |
| 389 | 363 | $type = sanitize_text_field( $type ); |
| 390 | 364 | $result = false; |
| 391 | 365 | $array = $this->getRequest( $type . 'Messages', array() ); |
| 366 | + // These messages are built by the plugin and have been already sanitized. | |
| 367 | + // Trying to sanitize them again will break plugin functionality since some of the messages contain html. | |
| 392 | 368 | if ( is_array( $array ) && count( $array ) > 0 ) { |
| 393 | 369 | $result = true; |
| 394 | - // Escape at the sink so safety no longer depends on every caller | |
| 395 | - // pre-escaping its message arguments (see F-04). wp_kses_post keeps | |
| 396 | - // the intentional markup (links, styled spans) some notices embed | |
| 397 | - // while neutralising anything dangerous. It now wraps the wpautop() | |
| 398 | - // result rather than each message, so the escaping is applied to | |
| 399 | - // everything that is actually echoed. | |
| 400 | - $html = wpautop( implode( "\n", $array ) ); | |
| 401 | 370 | ?> |
| 402 | - <div class="tcmp-box-<?php echo esc_attr( strtolower( $type ) ); ?>"><?php echo wp_kses_post( $html ); ?></div> | |
| 371 | + <div class="tcmp-box-<?php echo strtolower( $type ); ?>"><?php echo wpautop( implode( "\n", $array ) ); ?></div> | |
| 403 | 372 | <?php |
| 404 | 373 | } |
| 405 | 374 | if ( $clean ) { |
| 406 | 375 | $this->removeRequest( $type . 'Messages' ); |