| @@ -1935,14 +1935,67 @@ | ||
| 1935 | 1935 | } |
| 1936 | 1936 | |
| 1937 | 1937 | } |
| 1938 | 1938 | |
| 1939 | +/** | |
| 1940 | + * Safely sanitize chatbot conversation input. | |
| 1941 | + * | |
| 1942 | + * SECURITY FIX (CVE WPBot Stored XSS ≤ 8.6.9): | |
| 1943 | + * The previous order was: wp_kses() → html_entity_decode() → htmlspecialchars(). | |
| 1944 | + * An attacker could submit entity-encoded payloads (<img onerror=...>) that | |
| 1945 | + * bypassed wp_kses (which saw inert text), were then decoded back into live markup | |
| 1946 | + * by html_entity_decode(), and survived into storage and the admin UI. | |
| 1947 | + * | |
| 1948 | + * Correct order: html_entity_decode() FIRST → wp_kses() → htmlspecialchars(). | |
| 1949 | + * wp_kses() now sees the real decoded markup and strips forbidden tags/attributes. | |
| 1950 | + * | |
| 1951 | + * @param string $data Raw conversation string (already wp_unslash'd by caller). | |
| 1952 | + * @return string Sanitized, entity-encoded string safe for DB storage. | |
| 1953 | + */ | |
| 1939 | 1954 | function qcld_wpbot_input_validation( $data ) { |
| 1940 | - $data = html_entity_decode($data); | |
| 1941 | - $data = trim($data); | |
| 1942 | - $data = stripslashes($data); | |
| 1943 | - $data = htmlspecialchars($data); | |
| 1955 | + // 1. Decode any entity-encoded HTML so wp_kses sees the real markup. | |
| 1956 | + $data = html_entity_decode( $data, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); | |
| 1957 | + $data = trim( $data ); | |
| 1958 | + $data = stripslashes( $data ); | |
| 1959 | + // 2. Sanitize with a strict allowlist — NOW operating on decoded markup. | |
| 1960 | + $data = wp_kses( $data, wpbot_get_safe_conversation_tags() ); | |
| 1961 | + // 3. Re-encode for safe DB storage; admin.js decodes for rendering. | |
| 1962 | + $data = htmlspecialchars( $data, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); | |
| 1944 | 1963 | return $data; |
| 1964 | +} | |
| 1965 | + | |
| 1966 | +/** | |
| 1967 | + * Returns the strict HTML allowlist for chatbot conversation content. | |
| 1968 | + * | |
| 1969 | + * Critically: no event-handler attributes (onerror, onclick, onload, etc.) are | |
| 1970 | + * allowed — wp_kses strips any attribute not explicitly listed here. | |
| 1971 | + * 'img' is intentionally omitted; bot responses that include images should use | |
| 1972 | + * safe URLs only and can be re-added with only 'src', 'alt', 'class' if needed. | |
| 1973 | + * | |
| 1974 | + * @return array<string, array<string, bool>> | |
| 1975 | + */ | |
| 1976 | +function wpbot_get_safe_conversation_tags() { | |
| 1977 | + return array( | |
| 1978 | + 'ul' => array( 'class' => true ), | |
| 1979 | + 'ol' => array( 'class' => true ), | |
| 1980 | + 'li' => array( 'class' => true, 'id' => true ), | |
| 1981 | + 'div' => array( 'class' => true, 'id' => true ), | |
| 1982 | + 'span' => array( 'class' => true, 'id' => true ), | |
| 1983 | + 'p' => array( 'class' => true ), | |
| 1984 | + 'br' => array(), | |
| 1985 | + 'strong' => array(), | |
| 1986 | + 'em' => array(), | |
| 1987 | + 'b' => array(), | |
| 1988 | + 'i' => array(), | |
| 1989 | + 'a' => array( | |
| 1990 | + 'href' => true, | |
| 1991 | + 'target' => true, | |
| 1992 | + 'rel' => true, | |
| 1993 | + 'class' => true, | |
| 1994 | + ), | |
| 1995 | + // 'img' intentionally excluded — prevents onerror/onload injection. | |
| 1996 | + // Add back with only 'src','alt','class' if bot image responses are needed. | |
| 1997 | + ); | |
| 1945 | 1998 | } |
| 1946 | 1999 | add_action('wp_ajax_qcld_small_talk_import', 'qcld_small_talk_import'); |
| 1947 | 2000 | function qcld_small_talk_import(){ |
| 1948 | 2001 | if ( ! current_user_can( 'manage_options' ) ) { |