Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changes/fixed/3184.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Modify url parsing to allow passing args through to query url
74 changes: 42 additions & 32 deletions crates/fuel-core/src/graphql_api/api_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ use axum::{
extract::{
DefaultBodyLimit,
Extension,
RawQuery,
},
http::{
HeaderValue,
Expand Down Expand Up @@ -83,10 +84,7 @@ use std::{
TcpListener,
},
pin::Pin,
sync::{
Arc,
OnceLock,
},
sync::Arc,
};
use tokio_stream::StreamExt;
use tower::limit::ConcurrencyLimitLayer;
Expand Down Expand Up @@ -323,8 +321,9 @@ where
let graphql_endpoint = "/v1/graphql";
let graphql_subscription_endpoint = "/v1/graphql-sub";

let graphql_playground =
|| render_graphql_playground(graphql_endpoint, graphql_subscription_endpoint);
let graphql_playground = |query: RawQuery| {
render_graphql_playground(graphql_endpoint, graphql_subscription_endpoint, query)
};

let router = Router::new()
.route("/v1/playground", get(graphql_playground))
Expand Down Expand Up @@ -373,43 +372,54 @@ where
))
}

/// Single initialization of the GraphQL playground HTML.
/// This is because the rendering and replacing is expensive
static GRAPHQL_PLAYGROUND_HTML: OnceLock<Arc<String>> = OnceLock::new();

fn _render_graphql_playground(
endpoint: &str,
subscription_endpoint: &str,
query: RawQuery,
) -> impl IntoResponse + Send + Sync {
let html = GRAPHQL_PLAYGROUND_HTML.get_or_init(|| {
let raw_html = GraphiQLSource::build()
.endpoint(endpoint)
.subscription_endpoint(subscription_endpoint)
.title("Fuel Graphql Playground")
.finish();

// this may not be necessary in the future,
// but we need it to patch: https://github.com/async-graphql/async-graphql/issues/1703
let raw_html = raw_html.replace(
"https://unpkg.com/graphiql/graphiql.min.js",
"https://unpkg.com/graphiql@3/graphiql.min.js",
);
let raw_html = raw_html.replace(
"https://unpkg.com/graphiql/graphiql.min.css",
"https://unpkg.com/graphiql@3/graphiql.min.css",
);

Arc::new(raw_html)
});
let qs = query
.0
.as_deref()
.filter(|q| !q.is_empty())
.map(|q| format!("?{q}"))
.unwrap_or_default();

let endpoint = format!("{}{}", endpoint, qs);
let subscription_endpoint = format!("{}{}", subscription_endpoint, qs);

// let html = GRAPHQL_PLAYGROUND_HTML.get_or_init(|| {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented-out code left from OnceLock removal

Low Severity

The line // let html = GRAPHQL_PLAYGROUND_HTML.get_or_init(|| { is a leftover comment from the removed OnceLock caching mechanism. This dead commented-out code adds noise and could confuse future readers about the intended design.

Fix in Cursor Fix in Web

let raw_html = GraphiQLSource::build()
.endpoint(&endpoint)
.subscription_endpoint(&subscription_endpoint)
.title("Fuel Graphql Playground")
.finish();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reflected XSS via unsanitized query string in HTML

Medium Severity

User-controlled raw query string from RawQuery is appended to the endpoint and passed directly into GraphiQLSource::build().endpoint(), which embeds it in a JavaScript context via a handlebars template. The Rust handlebars crate's default HTML escaping does not escape single quotes ('). Since the GraphiQL template likely uses single-quoted JavaScript string literals for the endpoint value, an attacker can craft a URL like /v1/playground?';alert(document.cookie);// to inject arbitrary JavaScript. Single quotes are valid in URL query components per RFC 3986 and are not percent-encoded by browsers, so the raw ' reaches the server and gets embedded in the HTML output without sanitization.

Fix in Cursor Fix in Web


Html(html.as_str())
// this may not be necessary in the future,
// but we need it to patch: https://github.com/async-graphql/async-graphql/issues/1703
let raw_html = raw_html.replace(
"https://unpkg.com/graphiql/graphiql.min.js",
"https://unpkg.com/graphiql@3/graphiql.min.js",
);
let raw_html = raw_html.replace(
"https://unpkg.com/graphiql/graphiql.min.css",
"https://unpkg.com/graphiql@3/graphiql.min.css",
);

let raw_html = raw_html.replace(
"const url = new URL(endpoint, window.location.origin);",
r#"const url = new URL(endpoint, window.location.origin);
url.search = window.location.search;"#,
);

Html(raw_html.clone())
}

async fn render_graphql_playground(
endpoint: &str,
subscription_endpoint: &str,
raw_query: RawQuery,
) -> impl IntoResponse + Send + Sync {
_render_graphql_playground(endpoint, subscription_endpoint)
_render_graphql_playground(endpoint, subscription_endpoint, raw_query)
}

async fn health() -> Json<serde_json::Value> {
Expand Down
Loading