diff --git a/.changeset/hip-flies-camp.md b/.changeset/hip-flies-camp.md new file mode 100644 index 0000000..4c7e384 --- /dev/null +++ b/.changeset/hip-flies-camp.md @@ -0,0 +1,11 @@ +--- +"wpgraphql-ide": patch +--- + +### Added + +- Introduced `wp_localize_escaped_data()` function for recursively escaping data before localizing it in WordPress. This ensures safe output of strings, URLs, integers, and nested arrays when passing data to JavaScript, using native WordPress functions like `wp_kses_post()` and `esc_url()`. + +### Improved + +- Enhanced security by ensuring all localized data is properly sanitized before being passed to `wp_localize_script()`, preventing potential XSS vulnerabilities and ensuring safe use of dynamic data in JavaScript. diff --git a/wpgraphql-ide.php b/wpgraphql-ide.php index 5031c8b..9153060 100644 --- a/wpgraphql-ide.php +++ b/wpgraphql-ide.php @@ -341,7 +341,7 @@ function reorder_graphql_submenu_items(): void { $ordered_submenu[] = $graphql_ide; } if ( 'on' === $show_legacy_editor && $graphiql_ide ) { - $graphiql_ide[0] = 'Legacy GraphQL IDE'; + $graphiql_ide[0] = esc_html__( 'Legacy GraphQL IDE', 'wpgraphql-ide' ); $ordered_submenu[] = $graphiql_ide; } if ( $extensions ) { @@ -386,7 +386,7 @@ function enqueue_graphql_ide_menu_icon_css(): void { } '; - wp_add_inline_style( 'admin-bar', $custom_css ); + wp_add_inline_style( 'admin-bar', wp_kses_post( $custom_css ) ); } /** @@ -445,10 +445,12 @@ function enqueue_react_app_with_styles(): void { 'dedicatedIdeBaseUrl' => get_dedicated_ide_base_url(), ]; + $escaped_data = wp_localize_escaped_data( $localized_data ); + wp_localize_script( 'wpgraphql-ide', 'WPGRAPHQL_IDE_DATA', - $localized_data + $escaped_data ); // Extensions looking to extend GraphiQL can hook in here, @@ -505,6 +507,46 @@ function get_plugin_header( string $key = '' ): ?string { return is_string( $plugin_header ) ? $plugin_header : null; } +/** + * Retrieves and sanitizes external fragments. + * + * @return array The sanitized array of external fragments. + */ +function get_external_fragments(): array { + // Retrieve external fragments using the filter. + $external_fragments = apply_filters( 'wpgraphql_ide_external_fragments', [] ); + + // Loop through each fragment, sanitize, and ensure it's a valid GraphQL fragment. + return array_filter( + array_map( 'sanitize_text_field', $external_fragments ), + static function ( string $fragment ): bool { + // Check if the fragment starts with "fragment" and contains "on" (basic GraphQL fragment validation). + return preg_match( '/^fragment\s+\w+\s+on\s+\w+\s*{/', trim( $fragment ) ) === 1; + } + ); +} + +/** + * Recursive function to escape an array or value for safe output, specifically for localizing data in WordPress. + * + * @param mixed $data The data to escape. + * @return mixed The escaped data. + */ +function wp_localize_escaped_data( $data ) { + if ( is_array( $data ) ) { + return array_map( __NAMESPACE__ . '\wp_localize_escaped_data', $data ); + } elseif ( is_string( $data ) ) { + // Use wp_kses_post to allow basic HTML for content and esc_url for URLs + return filter_var( $data, FILTER_VALIDATE_URL ) ? esc_url( $data ) : wp_kses_post( $data ); + } elseif ( is_int( $data ) ) { + return absint( $data ); + } elseif ( is_bool( $data ) ) { + return (bool) $data; + } + + return $data; // Return original value if it's not a string, int, or bool. +} + /** * Retrieves app context. * @@ -514,18 +556,17 @@ function get_app_context(): array { $current_user = wp_get_current_user(); // Get the avatar URL for the current user. Returns an empty string if no user is logged in. - $avatar_url = $current_user->exists() ? get_avatar_url( $current_user->ID ) : ''; - - return apply_filters( - 'wpgraphql_ide_context', - [ - 'pluginVersion' => get_plugin_header( 'Version' ), - 'pluginName' => get_plugin_header( 'Name' ), - 'externalFragments' => apply_filters( 'wpgraphql_ide_external_fragments', [] ), - 'avatarUrl' => $avatar_url, - 'drawerButtonLabel' => __( 'GraphQL IDE', 'wpgraphql-ide' ), - ] - ); + $avatar_url = $current_user->exists() ? ( get_avatar_url( $current_user->ID ) ?: '' ) : ''; + + $app_context = [ + 'pluginVersion' => get_plugin_header( 'Version' ), + 'pluginName' => get_plugin_header( 'Name' ), + 'externalFragments' => get_external_fragments(), + 'avatarUrl' => $avatar_url, + 'drawerButtonLabel' => __( 'GraphQL IDE', 'wpgraphql-ide' ), + ]; + + return apply_filters( 'wpgraphql_ide_context', $app_context ); } /** @@ -558,7 +599,7 @@ function graphql_admin_notices_render_notices( array $notices ): void { // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_register_style( 'wpgraphql-ide-admin-notices', false ); wp_enqueue_style( 'wpgraphql-ide-admin-notices' ); - wp_add_inline_style( 'wpgraphql-ide-admin-notices', $custom_css ); + wp_add_inline_style( 'wpgraphql-ide-admin-notices', wp_kses_post( $custom_css ) ); } /**