Skip to content

Commit 9f83ef1

Browse files
committed
Merge github.com:prisma-labs/graphql-playground
2 parents 48da419 + b8e2688 commit 9f83ef1

File tree

14 files changed

+224
-25
lines changed

14 files changed

+224
-25
lines changed

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
> **SECURITY WARNING:** This `graphql-playground-html` and [all of it's middleware dependents](#impacted-packages) in this repository **had a severe XSS Reflection attack vulnerability to unsanitized user input** until version `[email protected]`. Impacted are any and all **user-defined** input to `renderPlaygroundPage()`, `koaPlayground()`,`expressPlayground()`, `koaPlayground()`, or `lambdaPlayground()`. If you used static values, such as `graphql-playground-electron` does in [it's webpack config](https://github.com/prisma-labs/graphql-playground/blob/master/packages/graphql-playground-electron/webpack.config.build.js#L16), you were not vulnerable to the attack. [More Details](./SECURITY.md)
1+
> **SECURITY WARNING:** both `graphql-playground-html` and [all four (4) of it's middleware dependents](#impacted-packages) until `[email protected]` were subject to an **XSS Reflection attack vulnerability only to unsanitized user input strings** to the functions therein. This was resolved in `graphql-playground-html@^1.6.22`. [More Information](#security-details)
2+
23

34
<p align="center"><img src="https://imgur.com/5fzMbyV.png" width="269"></p>
45

@@ -28,14 +29,23 @@ $ brew cask install graphql-playground
2829
- 🚥 Apollo Tracing support
2930

3031
## Security Details
31-
32-
**NOTE: only _unsanitized user input_ to the functions in these packages is vulnerable** to the recently reported XSS Reflection attack.
32+
> **NOTE: only _unsanitized user input_ to the functions in these packages is vulnerable** to the recently reported XSS Reflection attack.
3333
3434
### Impact
3535

36-
The only reason this vulnerability exists is because we are using template strings in `renderPlaygroundPage()` with potentially user defined variables. This allows an attacker to inject html and javascript into a page on execution.
36+
> Impacted are any and all unsanitized **user-defined** input to:
37+
-`renderPlaygroundPage()`
38+
-`koaPlayground()`
39+
-`expressPlayground()`
40+
-`koaPlayground()`
41+
-`lambdaPlayground()
42+
43+
> If you used static values, such as `graphql-playground-electron` does in [it's webpack config](https://github.com/prisma-labs/graphql-playground/blob/master/packages/graphql-playground-electron/webpack.config.build.js#L16), as well as the most common middleware implementations out there, they were not vulnerable to the attack.
3744
38-
Common examples may be user-defined path parameters, query string, unsanitized UI provided values in database, etc that are used to build template strings or passed directly to a `renderPlaygroundPage()` or the matching middleware function equivalent.
45+
The only reason this vulnerability exists is because we are using template strings in `renderPlaygroundPage()` with potentially unsanitized user defined variables. This allows an attacker to inject html and javascript into the page.
46+
- [Read more about preventing XSS in react](https://pragmaticwebsecurity.com/files/cheatsheets/reactxss.pdf)
47+
48+
Common examples may be user-defined path parameters, query string, unsanitized UI provided values in database, etc., that are used to build template strings or passed directly to a `renderPlaygroundPage()` or the matching middleware function equivalent listed above.
3949

4050
### Impacted Packages
4151

@@ -53,6 +63,8 @@ Common examples may be user-defined path parameters, query string, unsanitized U
5363

5464
See the [security docs](./SECURITY.md) for more details on how your implementation might be impacted by this vulnerability. It contains safe examples, unsafe examples, workarounds, and more details.
5565

66+
We've also provided ['an example of the xss using the express middleware]('https://github.com/prisma-labs/graphql-playground/tree/master/packages/graphql-playground-html/examples/xss-attack')
67+
5668
## FAQ
5769

5870
### How is this different from [GraphiQL](https://github.com/graphql/graphiql)?

SECURITY.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ When using
1313
- `expressPlayground()`
1414
- `koaPlayground()`
1515
- `lambdaPlayground()`
16-
- any downstream dependents that use these functions
16+
- any downstream dependent packages that use these functions
1717

1818
without sanitization of user input, your application is vulnerable to an XSS Reflecton Attack. This is a serious vulnerability that could allow for exfiltration of data or user credentials, or to disrupt systems.
1919

20+
We've provided ['an example of the xss using the express middleware]('https://github.com/prisma-labs/graphql-playground/tree/master/packages/graphql-playground-middleware-express/examples/xss-attack')
21+
2022
### Impacted Packages
2123

2224
**All versions of these packages are impacted until those specified below**, which are now safe for user defined input:
@@ -99,6 +101,8 @@ app.get('/playground', (req) =>
99101
)
100102
```
101103

104+
[See a proof of concept](packages/graphql-playground-html/examples/xss-attack) to understand the vulnerability better
105+
102106
### Workaround
103107

104108
To fix this issue without the update, you can sanitize however you want.
@@ -130,3 +134,5 @@ app.get('/playground/:id', (req) =>
130134
app.get('/playground', (req) =>
131135
expressPlayground(JSON.parse(filter(JSON.stringify(req.query))))
132136
```
137+
138+
[See a proof of concept workaround](packages/graphql-playground-html/examples/xss-attack), example #3
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# GraphQL Playground HTML XSS Reflection Attack Example
2+
3+
This shows the simplest possible example for how one might re-create the XSS Reflection Vulnerability reported by Cure53.
4+
5+
Notice we force the resolution to `[email protected]`, which is the last version susceptible. All prior versions are susceptible to the attack.
6+
7+
Dynamic, unsanitized input that resembles some of the configuration you see is a simple example - if url parameters, query parameters, unsanitized database text strings, etc are passed to `expressPlayground()`, `renderPlaygroundPage()` or equivalent middleware functions such as `koaPlayground()`, they are all vulnerable to this attack.
8+
9+
## Reccomendations
10+
11+
Here we use `xss` because it was easy to provide for node.js, however [DOMPurify](https://github.com/cure53/DOMPurify) is also an excellent choice for sanitizing strings for unwanted html. By default it requires the browser DOM, but you can load it with JSDOM for server side purposes as well.
12+
13+
here are a few more tips to prevent other XSS vulnerabilities that might exist in your own applications:
14+
15+
- `DOMPurify.sanitize` url values with user input to be used for rendering `<a href=` or `<script src=`, `<img src=` etc. whether using react or not!
16+
- in react,`dangerouslySetInnerHtml={{ __html: { DOMPurify.sanitize(userInputString) }}` always!
17+
- when doing direct dom manipulation, avoid `domElement.innerHTML = string` at all costs, but at least `DOMPurify.sanitize(string)` first if you must
18+
- when generating an entire html file, sanitize *all* user input values (this was our mistake)
19+
20+
## Setup
21+
22+
```sh
23+
$ yarn
24+
$ yarn start
25+
```
26+
27+
## Examples
28+
29+
Now that you've set up the example, you can view the examples:
30+
31+
### Example 1 - Query params
32+
33+
this one uses query parameters
34+
35+
http://localhost:4000/example-1?id=%3C/script%3E%3Cscript%3Ealert('I%20%3C3%20GraphQL.%20Hack%20the%20Planet!!')%3C/script%3E%3Cscript%3E
36+
37+
### Example 2 - DB Example
38+
39+
this one uses a mock database, to demonstrate more ways in which the function is susceptible
40+
41+
http://localhost:4000/example-2
42+
43+
### Example 3 - Upgrade workaround example
44+
45+
this one uses query like number 1, but shows how to use [`xss`](https://npmjs.com/xss) module to workaround the issue if you aren't able to upgrade
46+
47+
http://localhost:4000/example-3?id=%3C/script%3E%3Cscript%3Ealert('I%20%3C3%20GraphQL.%20Hack%20the%20Planet!!')%3C/script%3E%3Cscript%3E
48+
49+
### Example 4 - Always Safe example
50+
51+
this one uses static values, so it's safe without any workarounds! (try removing the ?darkMode parameter)
52+
53+
http://localhost:4000/example-4?darkMode=%3C/script%3E%3Cscript%3Ealert('I%20%3C3%20GraphQL.%20Hack%20the%20Planet!!')%3C/script%3E%3Cscript%3E
54+
55+
[XSS Safe using static configuration strings]("http://localhost:4000/example-3?darkMode")
56+
57+
## More Details
58+
59+
See more details in [SECURITY.md](../../../../SECURITY.md)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
const express = require('express')
2+
const { ApolloServer, gql } = require('apollo-server-express')
3+
const { filterXSS } = require('xss')
4+
const { renderPlaygroundPage } = require('graphql-playground-html')
5+
6+
const typeDefs = gql`
7+
type Query {
8+
hello: String!
9+
}
10+
schema {
11+
query: Query
12+
}
13+
`
14+
const resolvers = {
15+
Query: {
16+
hello: () => 'world',
17+
},
18+
}
19+
20+
const PORT = 4000
21+
22+
const server = new ApolloServer({ typeDefs, resolvers })
23+
24+
const app = express()
25+
server.applyMiddleware({ app })
26+
27+
// Example 1: Query Parameters
28+
29+
app.get('/example-1', (req, res, next) => {
30+
res.write(
31+
renderPlaygroundPage({
32+
endpoint: `/graphql/${req.query.id}`,
33+
}),
34+
)
35+
res.status(200)
36+
next()
37+
})
38+
39+
// Example 2: mock database example
40+
41+
const db = {
42+
async get() {
43+
return {
44+
'editor.fontFamily': `</script><script>alert('I <3 GraphQL. Hack the Planet!!')</script><script>`,
45+
}
46+
},
47+
}
48+
49+
app.get('/example-2', async (req, res, next) => {
50+
const settings = await db.get()
51+
res.write(
52+
renderPlaygroundPage({
53+
endpoint: '/graphql',
54+
settings,
55+
}),
56+
)
57+
next()
58+
})
59+
60+
// Example 3: Manual Workaround
61+
62+
const filter = (val) => {
63+
return filterXSS(val, {
64+
// @ts-ignore
65+
whiteList: [],
66+
stripIgnoreTag: true,
67+
stripIgnoreTagBody: ['script'],
68+
})
69+
}
70+
71+
app.get('/example-3', (req, res, next) => {
72+
res.write(
73+
renderPlaygroundPage({
74+
endpoint: `/graphql/${filter(req.query.id)}`,
75+
}),
76+
)
77+
res.status(200)
78+
next()
79+
})
80+
81+
// Example 4: Safe
82+
83+
app.get('/example-4', (req, res, next) => {
84+
res.write(
85+
renderPlaygroundPage({
86+
endpoint: `/graphql`,
87+
settings: {
88+
'editor.theme': req.query.darkMode ? 'dark' : 'light',
89+
},
90+
}),
91+
)
92+
res.status(200)
93+
next()
94+
})
95+
96+
app.listen(PORT)
97+
98+
console.log(
99+
`Serving the GraphQL Playground on http://localhost:${PORT}/example-2`,
100+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "xss-attack",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"start": "node index.js"
8+
},
9+
"keywords": [],
10+
"author": "",
11+
"license": "ISC",
12+
"dependencies": {
13+
"apollo-server-express": "^2.16.0",
14+
"express": "^4.17.1",
15+
"graphql": "^15.0.0",
16+
"graphql-playground-html": "1.6.20",
17+
"xss": "^1.0.6"
18+
}
19+
}

packages/graphql-playground-html/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "graphql-playground-html",
3-
"version": "1.6.24",
3+
"version": "1.6.25",
44
"homepage": "https://github.com/graphcool/graphql-playground/tree/master/packages/graphql-playground-html",
55
"description": "GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration).",
66
"contributors": [

packages/graphql-playground-html/src/render-playground-page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ const loading = getLoadingMarkup()
8989
const CONFIG_ID = 'playground-config';
9090

9191
const getCdnMarkup = ({ version, cdnUrl = '//cdn.jsdelivr.net/npm', faviconUrl }) => {
92-
const buildCDNUrl = (packageName: string, suffix: string) => filter(`${cdnUrl}/${packageName}/${version ? `@${version}/` : ''}${suffix}` || '')
92+
const buildCDNUrl = (packageName: string, suffix: string) => filter(`${cdnUrl}/${packageName}${version ? `@${version}` : ''}/${suffix}` || '')
9393
return `
9494
<link
9595
rel="stylesheet"

packages/graphql-playground-middleware-express/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "graphql-playground-middleware-express",
3-
"version": "1.7.17",
3+
"version": "1.7.18",
44
"homepage": "https://github.com/graphcool/graphql-playground/tree/master/packages/graphql-playground-middleware-express",
55
"description": "GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration).",
66
"contributors": [
@@ -34,7 +34,7 @@
3434
"typescript": "3.8.3"
3535
},
3636
"dependencies": {
37-
"graphql-playground-html": "^1.6.23"
37+
"graphql-playground-html": "1.6.25"
3838
},
3939
"typings": "dist/index.d.ts",
4040
"typescript": {

packages/graphql-playground-middleware-hapi/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "graphql-playground-middleware-hapi",
3-
"version": "1.6.14",
3+
"version": "1.6.15",
44
"homepage": "https://github.com/graphcool/graphql-playground/tree/master/packages/graphql-playground-middleware-hapi",
55
"description": "GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration).",
66
"contributors": [
@@ -40,6 +40,6 @@
4040
"definition": "dist/index.d.ts"
4141
},
4242
"dependencies": {
43-
"graphql-playground-html": "^1.6.23"
43+
"graphql-playground-html": "1.6.25"
4444
}
4545
}

packages/graphql-playground-middleware-koa/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "graphql-playground-middleware-koa",
3-
"version": "1.6.16",
3+
"version": "1.6.17",
44
"homepage": "https://github.com/graphcool/graphql-playground/tree/master/packages/graphql-playground-middleware-koa",
55
"description": "GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration).",
66
"contributors": [
@@ -39,6 +39,6 @@
3939
"definition": "dist/index.d.ts"
4040
},
4141
"dependencies": {
42-
"graphql-playground-html": "^1.6.23"
42+
"graphql-playground-html": "1.6.25"
4343
}
4444
}

packages/graphql-playground-middleware-koa/src/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import { Context } from 'koa'
1+
import { Context, Next } from 'koa'
22
import {
33
MiddlewareOptions,
44
renderPlaygroundPage,
55
} from 'graphql-playground-html'
66

7-
/* tslint:disable-next-line */
7+
export declare type KoaPlaygroundMiddlewareOptions = MiddlewareOptions
88

9-
export type KoaPlaygroundMiddleware = (ctx: Context, next: () => void) => void
9+
/* tslint:disable-next-line */
10+
export type KoaPlaygroundMiddleware = (ctx: Context, next: Next) => Promise<void>
1011

11-
export type Register = (options: MiddlewareOptions) => KoaPlaygroundMiddleware
12+
export type Register = (options: KoaPlaygroundMiddlewareOptions) => KoaPlaygroundMiddleware
1213

1314
const koa: Register = options => {
1415
return async function voyager(ctx, next) {

packages/graphql-playground-middleware-lambda/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "graphql-playground-middleware-lambda",
3-
"version": "1.7.18",
4-
"homepage": "https://github.com/graphcool/graphql-playground/tree/master/packages/graphql-playground-middleware-lambada",
3+
"version": "1.7.19",
4+
"homepage": "https://github.com/graphcool/graphql-playground/tree/master/packages/graphql-playground-middleware-lambda",
55
"description": "GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration).",
66
"contributors": [
77
"Tim Suchanek <[email protected]>",
@@ -39,6 +39,6 @@
3939
"definition": "dist/index.d.ts"
4040
},
4141
"dependencies": {
42-
"graphql-playground-html": "^1.6.23"
42+
"graphql-playground-html": "1.6.25"
4343
}
4444
}

packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import { share } from '../../../state/sharing/actions'
2323
import { openHistory } from '../../../state/general/actions'
2424
import { getSettings } from '../../../state/workspace/reducers'
25+
import { Session } from '../../../state/sessions/reducers'
2526
import { ISettings } from '../../../types'
2627

2728
export interface Props {
@@ -30,6 +31,7 @@ export interface Props {
3031
fixedEndpoint?: boolean
3132
isPollingSchema: boolean
3233
endpointUnreachable: boolean
34+
session: Session
3335

3436
editEndpoint: (value: string) => void
3537
prettifyQuery: () => void
@@ -111,8 +113,7 @@ class TopBar extends React.Component<Props, {}> {
111113
this.props.openHistory()
112114
}
113115
getCurl = () => {
114-
// no need to rerender the whole time. only on-demand the store is fetched
115-
const session = getSelectedSession(this.context.store.getState())
116+
const session = this.props.session
116117
let variables
117118
try {
118119
variables = JSON.parse(session.variables)
@@ -157,6 +158,7 @@ const mapStateToProps = createStructuredSelector({
157158
isPollingSchema: getIsPollingSchema,
158159
endpointUnreachable: getEndpointUnreachable,
159160
settings: getSettings,
161+
session: getSelectedSession,
160162
})
161163

162164
export default connect(

0 commit comments

Comments
 (0)