From 2c85a137a51e6330bb2a68686091d29a16fa808a Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 18 Nov 2019 11:45:48 +0100 Subject: [PATCH] implement client side xsrf protection --- scm-ui/ui-components/src/apiclient.test.ts | 21 ++++++++++++- scm-ui/ui-components/src/apiclient.ts | 36 ++++++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/scm-ui/ui-components/src/apiclient.test.ts b/scm-ui/ui-components/src/apiclient.test.ts index 93f535728f..de9cf77dc3 100644 --- a/scm-ui/ui-components/src/apiclient.test.ts +++ b/scm-ui/ui-components/src/apiclient.test.ts @@ -1,4 +1,4 @@ -import { apiClient, createUrl } from "./apiclient"; +import { apiClient, createUrl, extractXsrfToken, extractXsrfTokenFromCookie } from "./apiclient"; import fetchMock from "fetch-mock"; import { BackendError } from "./errors"; @@ -70,3 +70,22 @@ describe("error handling tests", () => { }); }); }); + +describe("extract xsrf token", () => { + it("should return undefined if no cookie exists", () => { + const token = extractXsrfTokenFromCookie(undefined); + expect(token).toBeUndefined(); + }); + + it("should return undefined without X-Bearer-Token exists", () => { + const token = extractXsrfTokenFromCookie("a=b; c=d; e=f"); + expect(token).toBeUndefined(); + }); + + it("should return xsrf token", () => { + const cookie = + "a=b; X-Bearer-Token=eyJhbGciOiJIUzI1NiJ9.eyJ4c3JmIjoiYjE0NDRmNWEtOWI5Mi00ZDA0LWFkMzMtMTAxYjY3MWQ1YTc0Iiwic3ViIjoic2NtYWRtaW4iLCJqdGkiOiI2RFJpQVphNWwxIiwiaWF0IjoxNTc0MDcyNDQ4LCJleHAiOjE1NzQwNzYwNDgsInNjbS1tYW5hZ2VyLnJlZnJlc2hFeHBpcmF0aW9uIjoxNTc0MTE1NjQ4OTU5LCJzY20tbWFuYWdlci5wYXJlbnRUb2tlbklkIjoiNkRSaUFaYTVsMSJ9.VUJtKeWUn3xtHCEbG51r7ceXZ8CF3cmN8J-eb9EDY_U; c=d"; + const token = extractXsrfTokenFromCookie(cookie); + expect(token).toBe("b1444f5a-9b92-4d04-ad33-101b671d5a74"); + }); +}); diff --git a/scm-ui/ui-components/src/apiclient.ts b/scm-ui/ui-components/src/apiclient.ts index 8af093fac7..560b7c5722 100644 --- a/scm-ui/ui-components/src/apiclient.ts +++ b/scm-ui/ui-components/src/apiclient.ts @@ -2,14 +2,46 @@ import { contextPath } from "./urls"; import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError } from "./errors"; import { BackendErrorContent } from "./errors"; +const extractXsrfTokenFromJwt = (jwt: string) => { + const parts = jwt.split("."); + if (parts.length === 3) { + return JSON.parse(atob(parts[1])).xsrf; + } +}; + +// @VisibleForTesting +export const extractXsrfTokenFromCookie = (cookieString?: string) => { + if (cookieString) { + const cookies = cookieString.split(";"); + for (const c of cookies) { + const parts = c.trim().split("="); + if (parts[0] === "X-Bearer-Token") { + return extractXsrfTokenFromJwt(parts[1]); + } + } + } +}; + +const extractXsrfToken = () => { + return extractXsrfTokenFromCookie(document.cookie); +}; + const applyFetchOptions: (p: RequestInit) => RequestInit = o => { - o.credentials = "same-origin"; - o.headers = { + const headers: { [key: string]: string } = { Cache: "no-cache", // identify the request as ajax request "X-Requested-With": "XMLHttpRequest", + // identify the web interface "X-SCM-Client": "WUI" }; + + const xsrf = extractXsrfToken(); + if (xsrf) { + headers["X-XSRF-Token"] = xsrf; + } + + o.credentials = "same-origin"; + o.headers = headers; return o; };