Skip to content

Commit 23a0d56

Browse files
committed
fix: XSS vulnerability in GraphiQL js_escape function
The js_escape function only escaped newlines and single quotes but not backslashes. This allowed an attacker to bypass the escaping with a backslash before a quote (e.g. \'), breaking out of the JavaScript string context and executing arbitrary code. The fix escapes backslashes first (before other characters), and also handles carriage returns and </script> injection. Fixes #275
1 parent 24ec7aa commit 23a0d56

2 files changed

Lines changed: 26 additions & 2 deletions

File tree

lib/absinthe/plug/graphiql.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,8 +395,11 @@ defmodule Absinthe.Plug.GraphiQL do
395395

396396
defp js_escape(string) do
397397
string
398-
|> String.replace(~r/\n/, "\\n")
399-
|> String.replace(~r/'/, "\\'")
398+
|> String.replace("\\", "\\\\")
399+
|> String.replace("'", "\\'")
400+
|> String.replace("\n", "\\n")
401+
|> String.replace("\r", "\\r")
402+
|> String.replace("</", "<\\/")
400403
end
401404

402405
defp handle_default_headers(config, conn) do

test/lib/absinthe/graphiql_test.exs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,27 @@ defmodule Absinthe.Plug.GraphiQLTest do
232232
assert String.contains?(body, "defaultWebsocketUrl: ''")
233233
end
234234

235+
test "query parameter is properly escaped against XSS" do
236+
opts = Absinthe.Plug.GraphiQL.init(schema: TestSchema)
237+
238+
# This payload would break out of a JS string if backslashes aren't escaped.
239+
# Without the fix: xxx\');confirm(document.domain);// would close the string.
240+
# With the fix: backslashes are escaped so the string stays intact.
241+
xss_payload = "xxx\\');confirm(document.domain);//"
242+
243+
assert %{status: 200, resp_body: body} =
244+
conn(:get, "/?query=#{URI.encode(xss_payload)}")
245+
|> plug_parser
246+
|> put_req_header("accept", "text/html")
247+
|> Absinthe.Plug.GraphiQL.call(opts)
248+
249+
# The payload must NOT appear as unquoted JS code.
250+
# With proper escaping, the backslash before the quote is escaped (\\'),
251+
# so the quote doesn't close the string and the code never executes.
252+
# We check that the escaped version is present (backslash is doubled).
253+
assert String.contains?(body, "xxx\\\\\\');confirm")
254+
end
255+
235256
defp plug_parser(conn) do
236257
opts =
237258
Plug.Parsers.init(

0 commit comments

Comments
 (0)