Skip to content

Commit 180f19a

Browse files
fix(ES-1214): fix XSS vulnerability (#7302)
* fix(ES-1214): fix XSS vulnerability * code refactor * refactor * sanitize all params
1 parent 2054857 commit 180f19a

File tree

10 files changed

+125
-41
lines changed

10 files changed

+125
-41
lines changed

.changeset/weak-impalas-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@vue-storefront/middleware": patch
3+
---
4+
5+
[FIXED] a potential XSS (Cross-Site Scripting) vulnerability in the middleware. Now, each parameter is properly sanitized and validated before being used in the middleware.

packages/middleware/__tests__/integration/createServer.spec.ts

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,33 @@ describe("[Integration] Create server", () => {
1818
res.send("Custom error handler");
1919
},
2020
location: "./__tests__/integration/bootstrap/server",
21-
extensions() {
22-
return [
21+
extensions: (extensions) =>
22+
[
23+
...extensions,
2324
{
2425
name: "my-extension",
2526
extendApiMethods: {
26-
myFunc(context) {
27+
myFunc: (context) => {
2728
return context.api.success();
2829
},
29-
myFuncWithDependencyToOtherExtension(context) {
30-
return (context.api as any).myFunc();
30+
myFuncWithDependencyToOtherExtension: (context) => {
31+
return context.api.myFunc();
3132
},
3233
},
3334
},
3435
{
3536
name: "my-namespaced-extension",
3637
isNamespaced: true,
3738
extendApiMethods: {
38-
myFunc(context) {
39+
myFunc: (context) => {
3940
return context.api.error();
4041
},
41-
myFuncNamespaced(context) {
42+
myFuncNamespaced: (context) => {
4243
return context.api.success();
4344
},
4445
},
4546
},
46-
];
47-
},
47+
] as any,
4848
},
4949
},
5050
});
@@ -208,4 +208,55 @@ describe("[Integration] Create server", () => {
208208
expect(status).toEqual(200);
209209
expect(response).toEqual(apiMethodResult);
210210
});
211+
212+
it("should accept only GET and POST methods", async () => {
213+
const getRes = await request(app).get("/test_integration/success").send();
214+
215+
expect(getRes.status).toEqual(200);
216+
expect(getRes.body.message).toEqual("ok");
217+
218+
const postRes = await request(app).post("/test_integration/success").send();
219+
220+
expect(postRes.status).toEqual(200);
221+
expect(postRes.body.message).toEqual("ok");
222+
223+
const putRes = await request(app).put("/test_integration/success").send();
224+
225+
expect(putRes.status).toEqual(405);
226+
expect(putRes.error && putRes.error.text).toEqual(
227+
"Method PUT is not allowed. Please, use GET or POST method."
228+
);
229+
230+
const deleteRes = await request(app)
231+
.delete("/test_integration/success")
232+
.send();
233+
234+
expect(deleteRes.status).toEqual(405);
235+
expect(deleteRes.error && deleteRes.error.text).toEqual(
236+
"Method DELETE is not allowed. Please, use GET or POST method."
237+
);
238+
});
239+
240+
describe("prevent XSS attacks", () => {
241+
test.each([
242+
[
243+
"/z--%3E%3C!--hi--%3E%3Cimg%20src=x%20onerror=alert('DOM--XSS')%3E%3C!--%3C%3C/success",
244+
`"z--&gt;<img src>" integration is not configured. Please, check the request path or integration configuration.`,
245+
],
246+
[
247+
"/test_integration/z--%3E%3C!--hi--%3E%3Cimg%20src=x%20onerror=alert('DOM--XSS')%3E%3C!--%3C%3C",
248+
`Failed to resolve apiClient or function: The function "z--&gt;<img src>" is not registered.`,
249+
],
250+
[
251+
"/test_integration/z--%3E%3C!--hi--%3E%3Cimg%20src=x%20onerror=alert('DOM--XSS')%3E%3C!--%3C%3C/success",
252+
`Failed to resolve apiClient or function: Extension "z--&gt;<img src>" is not namespaced or the function "success" is not available in the namespace.`,
253+
],
254+
])("Use case: %s", async (maliciousUrl, expectedMessage) => {
255+
const res = await request(app).get(maliciousUrl).send();
256+
expect(res.error && res.error.text).not.toContain(
257+
"z--><!--hi--><img src=x onerror=alert('DOM--XSS')><!--<<"
258+
);
259+
expect(res.error && res.error.text).toEqual(expectedMessage);
260+
});
261+
});
211262
});

packages/middleware/__tests__/unit/handlers/prepareApiFunction.spec.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,6 @@ describe("[middleware-handlers] prepareApiFunction", () => {
2929
res.locals = {};
3030
});
3131

32-
it("sends 404 error if integration is not configured", async () => {
33-
const emptyIntegrations = {};
34-
35-
await prepareApiFunction(emptyIntegrations)(req, res, next);
36-
37-
expect(res.status).toBeCalledTimes(1);
38-
expect(res.status).toBeCalledWith(404);
39-
expect(res.send).toBeCalledTimes(1);
40-
});
41-
4232
describe("if integration is configured", () => {
4333
it("adds api function to res.locals", async () => {
4434
await prepareApiFunction(integrations)(req, res, next);

packages/middleware/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"cors": "^2.8.5",
2626
"express": "^4.18.1",
2727
"helmet": "^5.1.1",
28-
"@godaddy/terminus": "^4.12.1"
28+
"@godaddy/terminus": "^4.12.1",
29+
"xss": "^1.0.15"
2930
},
3031
"devDependencies": {
3132
"@types/body-parser": "^1.19.2",

packages/middleware/src/createServer.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type { HelmetOptions } from "helmet";
66
import helmet from "helmet";
77
import http, { Server } from "node:http";
88
import { createTerminus } from "@godaddy/terminus";
9-
109
import { registerIntegrations } from "./integrations";
1110
import type {
1211
Helmet,
@@ -19,6 +18,7 @@ import {
1918
prepareErrorHandler,
2019
prepareArguments,
2120
callApiFunction,
21+
validateParams,
2222
} from "./handlers";
2323
import { createTerminusOptions } from "./terminus";
2424

@@ -66,15 +66,9 @@ async function createServer<
6666
const integrations = await registerIntegrations(app, config.integrations);
6767
consola.success("Integrations loaded!");
6868

69-
app.post(
70-
"/:integrationName/:extensionName?/:functionName",
71-
prepareApiFunction(integrations),
72-
prepareErrorHandler(integrations),
73-
prepareArguments,
74-
callApiFunction
75-
);
76-
app.get(
69+
app.all(
7770
"/:integrationName/:extensionName?/:functionName",
71+
validateParams(integrations),
7872
prepareApiFunction(integrations),
7973
prepareErrorHandler(integrations),
8074
prepareArguments,

packages/middleware/src/handlers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from "./callApiFunction";
22
export * from "./prepareApiFunction";
33
export * from "./prepareErrorHandler";
44
export * from "./prepareArguments";
5+
export * from "./validateParams";

packages/middleware/src/handlers/prepareApiFunction/index.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,6 @@ export function prepareApiFunction(
77
return async (req, res, next) => {
88
const { integrationName, extensionName, functionName } = req.params;
99

10-
if (!integrations || !integrations[integrationName]) {
11-
res.status(404);
12-
res.send(
13-
`"${integrationName}" integration is not configured. Please, check the request path or integration configuration.`
14-
);
15-
16-
return;
17-
}
18-
1910
const {
2011
apiClient,
2112
configuration,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import xss from "xss";
2+
import type { Request, Response, NextFunction } from "express";
3+
import { IntegrationsLoaded } from "../../types";
4+
5+
export function validateParams(integrations: IntegrationsLoaded) {
6+
return (req: Request, res: Response, next: NextFunction) => {
7+
// Validate & sanitize the request params
8+
Object.entries(req.params).forEach(([key, value]) => {
9+
req.params[key] = typeof value === "string" ? xss(value) : value;
10+
});
11+
12+
// Validate the request method
13+
const { method } = req;
14+
if (method !== "GET" && method !== "POST") {
15+
res.status(405);
16+
res.send(
17+
`Method ${method} is not allowed. Please, use GET or POST method.`
18+
);
19+
20+
return;
21+
}
22+
23+
// Validate the integration
24+
const { integrationName } = req.params;
25+
if (!integrations || !integrations[integrationName]) {
26+
res.status(404);
27+
res.send(
28+
`"${integrationName}" integration is not configured. Please, check the request path or integration configuration.`
29+
);
30+
31+
return;
32+
}
33+
34+
next();
35+
};
36+
}

packages/middleware/src/types/common.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,11 @@ export interface Integration<
7878
> {
7979
location: string;
8080
configuration: CONFIG;
81-
extensions?: <T extends ApiClientMethodWithContext<CONTEXT>>(
81+
extensions?: (
8282
extensions: ApiClientExtension<API, CONTEXT>[]
83-
) => ApiClientExtension<API & T, CONTEXT>[];
83+
// TODO(IN-4338): There is a bug in the types here
84+
// we're not able to verify if the methods are namespaced or not with this implementation.
85+
) => ApiClientExtension<API, CONTEXT>[];
8486
customQueries?: Record<string, CustomQueryFunction>;
8587
initConfig?: TObject;
8688
errorHandler?: (error: unknown, req: Request, res: Response) => void;

yarn.lock

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4629,7 +4629,7 @@ commander@^10.0.0:
46294629
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
46304630
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
46314631

4632-
commander@^2.18.0:
4632+
commander@^2.18.0, commander@^2.20.3:
46334633
version "2.20.3"
46344634
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
46354635
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -4809,6 +4809,11 @@ cssesc@^3.0.0:
48094809
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
48104810
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
48114811

4812+
4813+
version "0.0.10"
4814+
resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae"
4815+
integrity sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==
4816+
48124817
cssom@^0.4.4:
48134818
version "0.4.4"
48144819
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
@@ -13190,6 +13195,14 @@ xmlchars@^2.2.0:
1319013195
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
1319113196
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
1319213197

13198+
xss@^1.0.15:
13199+
version "1.0.15"
13200+
resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.15.tgz#96a0e13886f0661063028b410ed1b18670f4e59a"
13201+
integrity sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==
13202+
dependencies:
13203+
commander "^2.20.3"
13204+
cssfilter "0.0.10"
13205+
1319313206
xtend@^4.0.0:
1319413207
version "4.0.2"
1319513208
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"

0 commit comments

Comments
 (0)