| @@ -3,13 +3,10 @@ | ||
| 3 | 3 | exit; |
| 4 | 4 | } |
| 5 | 5 | |
| 6 | 6 | class TCMP_Options { |
| 7 | + public function __construct() { | |
| 7 | 8 | |
| 8 | - private $request_data; | |
| 9 | - | |
| 10 | - public function __construct() { | |
| 11 | - $this->request_data = array(); | |
| 12 | 9 | } |
| 13 | 10 | |
| 14 | 11 | //Cache |
| 15 | 12 | private function getCacheName( $array ) { |
| @@ -97,42 +94,24 @@ | ||
| 97 | 94 | $key = $this->get_key( $key ); |
| 98 | 95 | $wp_session[ $key ] = $value; |
| 99 | 96 | } |
| 100 | 97 | |
| 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). | |
| 98 | + //$_REQUEST | |
| 99 | + //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 | 100 | private function removeRequest( $key ) { |
| 126 | - unset( $this->request_data[ $key ] ); | |
| 101 | + $key = $this->get_key( $key ); | |
| 102 | + if ( isset( $_POST[ $key ] ) ) { | |
| 103 | + unset( $_POST[ $key ] ); | |
| 104 | + } | |
| 127 | 105 | } |
| 128 | 106 | private function getRequest( $key, $default = false ) { |
| 107 | + $key = $this->get_key( $key ); | |
| 129 | 108 | $result = $default; |
| 130 | - if ( isset( $this->request_data[ $key ] ) ) { | |
| 131 | - if ( is_object( $this->request_data[ $key ] ) ) { | |
| 132 | - $result = clone $this->request_data[ $key ]; | |
| 109 | + if ( isset( $_POST[ $key ] ) ) { | |
| 110 | + if ( is_object( $_POST[ $key ] ) ) { | |
| 111 | + $result = clone $_POST[ $key ]; | |
| 133 | 112 | } else { |
| 134 | - $result = $this->request_data[ $key ]; | |
| 113 | + $result = $_POST[ $key ]; | |
| 135 | 114 | } |
| 136 | 115 | $result = $this->recursive_wp_kses( $result ); |
| 137 | 116 | } |
| 138 | 117 | return $result; |
| @@ -137,25 +116,8 @@ | ||
| 137 | 116 | } |
| 138 | 117 | return $result; |
| 139 | 118 | } |
| 140 | 119 | |
| 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 | 120 | public function recursive_wp_kses( $array ) { |
| 159 | 121 | global $tcmp_allowed_html_tags; |
| 160 | 122 | foreach ( $array as $key => &$value ) { |
| 161 | 123 | if ( is_array( $value ) ) { |
| @@ -164,9 +126,9 @@ | ||
| 164 | 126 | if ( ! $this->getSkipCodeSanitization() ) { |
| 165 | 127 | $value = wp_kses( $value, $tcmp_allowed_html_tags ); |
| 166 | 128 | } |
| 167 | 129 | } elseif ( is_string( $value ) ) { |
| 168 | - $value = wp_kses_post( $value ); | |
| 130 | + $value = wp_kses( $value, $tcmp_allowed_html_tags ); | |
| 169 | 131 | } else { |
| 170 | 132 | // do nothing ... could be a video or graphics object |
| 171 | 133 | } |
| 172 | 134 | } |
| @@ -173,9 +135,10 @@ | ||
| 173 | 135 | return $array; |
| 174 | 136 | } |
| 175 | 137 | |
| 176 | 138 | private function setRequest( $key, $value ) { |
| 177 | - $this->request_data[ $key ] = $value; | |
| 139 | + $key = $this->get_key( $key ); | |
| 140 | + $_POST[ $key ] = $value; | |
| 178 | 141 | } |
| 179 | 142 | |
| 180 | 143 | public function isPluginFirstInstall() { |
| 181 | 144 | return $this->getOption( 'PluginFirstInstall', false ); |
| @@ -388,19 +351,14 @@ | ||
| 388 | 351 | private function writeGenericMessages( $type, $clean = true ) { |
| 389 | 352 | $type = sanitize_text_field( $type ); |
| 390 | 353 | $result = false; |
| 391 | 354 | $array = $this->getRequest( $type . 'Messages', array() ); |
| 355 | + // These messages are built by the plugin and have been already sanitized. | |
| 356 | + // Trying to sanitize them again will break plugin functionality since some of the messages contain html. | |
| 392 | 357 | if ( is_array( $array ) && count( $array ) > 0 ) { |
| 393 | 358 | $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 | 359 | ?> |
| 402 | - <div class="tcmp-box-<?php echo esc_attr( strtolower( $type ) ); ?>"><?php echo wp_kses_post( $html ); ?></div> | |
| 360 | + <div class="tcmp-box-<?php echo strtolower( $type ); ?>"><?php echo wpautop( implode( "\n", $array ) ); ?></div> | |
| 403 | 361 | <?php |
| 404 | 362 | } |
| 405 | 363 | if ( $clean ) { |
| 406 | 364 | $this->removeRequest( $type . 'Messages' ); |