diff --git a/README.md b/README.md index 0f4cfe31c2..7d11ea570f 100644 --- a/README.md +++ b/README.md @@ -201,9 +201,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
seoulaja

🌍
Maurits Lourens

⚠️ 💻 - -
Rory Powell

🚇 ⚠️ 💻 - diff --git a/lerna.json b/lerna.json index 74dc9d096f..523485d1f3 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.0.46-alpha.6", + "version": "1.0.46-alpha.8", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 4ea9f22797..a3448828bb 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.0.46-alpha.6", + "version": "1.0.46-alpha.8", "description": "Budibase backend core libraries used in server and worker", "main": "src/index.js", "author": "Budibase", diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.js index 7f66d887ae..41d2bb1cc5 100644 --- a/packages/backend-core/src/auth.js +++ b/packages/backend-core/src/auth.js @@ -12,6 +12,7 @@ const { tenancy, appTenancy, authError, + csrf, } = require("./middleware") // Strategies @@ -42,4 +43,5 @@ module.exports = { buildAppTenancyMiddleware: appTenancy, auditLog, authError, + buildCsrfMiddleware: csrf, } diff --git a/packages/backend-core/src/constants.js b/packages/backend-core/src/constants.js index 091e4337cf..559dc0e6b2 100644 --- a/packages/backend-core/src/constants.js +++ b/packages/backend-core/src/constants.js @@ -18,6 +18,7 @@ exports.Headers = { TYPE: "x-budibase-type", TENANT_ID: "x-budibase-tenant-id", TOKEN: "x-budibase-token", + CSRF_TOKEN: "x-csrf-token", } exports.GlobalRoles = { diff --git a/packages/backend-core/src/middleware/authenticated.js b/packages/backend-core/src/middleware/authenticated.js index 87bd4d35ce..4978f7b9dc 100644 --- a/packages/backend-core/src/middleware/authenticated.js +++ b/packages/backend-core/src/middleware/authenticated.js @@ -60,6 +60,7 @@ module.exports = ( } else { user = await getUser(userId, session.tenantId) } + user.csrfToken = session.csrfToken delete user.password authenticated = true } catch (err) { diff --git a/packages/backend-core/src/middleware/csrf.js b/packages/backend-core/src/middleware/csrf.js new file mode 100644 index 0000000000..12bd9473e6 --- /dev/null +++ b/packages/backend-core/src/middleware/csrf.js @@ -0,0 +1,78 @@ +const { Headers } = require("../constants") +const { buildMatcherRegex, matches } = require("./matchers") + +/** + * GET, HEAD and OPTIONS methods are considered safe operations + * + * POST, PUT, PATCH, and DELETE methods, being state changing verbs, + * should have a CSRF token attached to the request + */ +const EXCLUDED_METHODS = ["GET", "HEAD", "OPTIONS"] + +/** + * There are only three content type values that can be used in cross domain requests. + * If any other value is used, e.g. application/json, the browser will first make a OPTIONS + * request which will be protected by CORS. + */ +const INCLUDED_CONTENT_TYPES = [ + "application/x-www-form-urlencoded", + "multipart/form-data", + "text/plain", +] + +/** + * Validate the CSRF token generated aganst the user session. + * Compare the token with the x-csrf-token header. + * + * If the token is not found within the request or the value provided + * does not match the value within the user session, the request is rejected. + * + * CSRF protection provided using the 'Synchronizer Token Pattern' + * https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern + * + */ +module.exports = (opts = { noCsrfPatterns: [] }) => { + const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns) + return async (ctx, next) => { + // don't apply for excluded paths + const found = matches(ctx, noCsrfOptions) + if (found) { + return next() + } + + // don't apply for the excluded http methods + if (EXCLUDED_METHODS.indexOf(ctx.method) !== -1) { + return next() + } + + // don't apply when the content type isn't supported + let contentType = ctx.get("content-type") + ? ctx.get("content-type").toLowerCase() + : "" + if ( + !INCLUDED_CONTENT_TYPES.filter(type => contentType.includes(type)).length + ) { + return next() + } + + // don't apply csrf when the internal api key has been used + if (ctx.internal) { + return next() + } + + // apply csrf when there is a token in the session (new logins) + // in future there should be a hard requirement that the token is present + const userToken = ctx.user.csrfToken + if (!userToken) { + return next() + } + + // reject if no token in request or mismatch + const requestToken = ctx.get(Headers.CSRF_TOKEN) + if (!requestToken || requestToken !== userToken) { + ctx.throw(403, "Invalid CSRF token") + } + + return next() + } +} diff --git a/packages/backend-core/src/middleware/index.js b/packages/backend-core/src/middleware/index.js index 7ea07a21ce..0d01fb3952 100644 --- a/packages/backend-core/src/middleware/index.js +++ b/packages/backend-core/src/middleware/index.js @@ -8,6 +8,7 @@ const auditLog = require("./auditLog") const tenancy = require("./tenancy") const appTenancy = require("./appTenancy") const datasourceGoogle = require("./passport/datasource/google") +const csrf = require("./csrf") module.exports = { google, @@ -22,4 +23,5 @@ module.exports = { datasource: { google: datasourceGoogle, }, + csrf, } diff --git a/packages/backend-core/src/security/sessions.js b/packages/backend-core/src/security/sessions.js index ad21627bd9..bbe6be299d 100644 --- a/packages/backend-core/src/security/sessions.js +++ b/packages/backend-core/src/security/sessions.js @@ -1,4 +1,5 @@ const redis = require("../redis/authRedis") +const { v4: uuidv4 } = require("uuid") // a week in seconds const EXPIRY_SECONDS = 86400 * 7 @@ -16,6 +17,9 @@ function makeSessionID(userId, sessionId) { exports.createASession = async (userId, session) => { const client = await redis.getSessionClient() const sessionId = session.sessionId + if (!session.csrfToken) { + session.csrfToken = uuidv4() + } session = { createdAt: new Date().toISOString(), lastAccessedAt: new Date().toISOString(), diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 074d73c939..94c8cab3f6 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "1.0.46-alpha.6", + "version": "1.0.46-alpha.8", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/builder/package.json b/packages/builder/package.json index 17f737f89b..cdc5385fb6 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "1.0.46-alpha.6", + "version": "1.0.46-alpha.8", "license": "GPL-3.0", "private": true, "scripts": { @@ -66,10 +66,10 @@ } }, "dependencies": { - "@budibase/bbui": "^1.0.46-alpha.6", - "@budibase/client": "^1.0.46-alpha.6", + "@budibase/bbui": "^1.0.46-alpha.8", + "@budibase/client": "^1.0.46-alpha.8", "@budibase/colorpicker": "1.1.2", - "@budibase/string-templates": "^1.0.46-alpha.6", + "@budibase/string-templates": "^1.0.46-alpha.8", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/builderStore/api.js b/packages/builder/src/builderStore/api.js index a5c6ceba54..a932799701 100644 --- a/packages/builder/src/builderStore/api.js +++ b/packages/builder/src/builderStore/api.js @@ -1,12 +1,20 @@ import { store } from "./index" import { get as svelteGet } from "svelte/store" import { removeCookie, Cookies } from "./cookies" +import { auth } from "stores/portal" const apiCall = method => async (url, body, headers = { "Content-Type": "application/json" }) => { headers["x-budibase-app-id"] = svelteGet(store).appId headers["x-budibase-api-version"] = "1" + + // add csrf token if authenticated + const user = svelteGet(auth).user + if (user && user.csrfToken) { + headers["x-csrf-token"] = user.csrfToken + } + const json = headers["Content-Type"] === "application/json" const resp = await fetch(url, { method: method, diff --git a/packages/builder/src/pages/builder/auth/reset.svelte b/packages/builder/src/pages/builder/auth/reset.svelte index e38a5d8b24..f78dd19eb9 100644 --- a/packages/builder/src/pages/builder/auth/reset.svelte +++ b/packages/builder/src/pages/builder/auth/reset.svelte @@ -31,6 +31,7 @@ } onMount(async () => { + await auth.checkAuth() await organisation.init() }) diff --git a/packages/cli/package.json b/packages/cli/package.json index e98bf37d9c..7b02cf7296 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "1.0.46-alpha.6", + "version": "1.0.46-alpha.8", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { diff --git a/packages/client/package.json b/packages/client/package.json index 1e380603ef..c6704a5151 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "1.0.46-alpha.6", + "version": "1.0.46-alpha.8", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "^1.0.46-alpha.6", + "@budibase/bbui": "^1.0.46-alpha.8", "@budibase/standard-components": "^0.9.139", - "@budibase/string-templates": "^1.0.46-alpha.6", + "@budibase/string-templates": "^1.0.46-alpha.8", "regexparam": "^1.3.0", "rollup-plugin-polyfill-node": "^0.8.0", "shortid": "^2.2.15", diff --git a/packages/client/src/api/api.js b/packages/client/src/api/api.js index d43ff8b20c..1bb12cca53 100644 --- a/packages/client/src/api/api.js +++ b/packages/client/src/api/api.js @@ -1,4 +1,5 @@ -import { notificationStore } from "stores" +import { notificationStore, authStore } from "stores" +import { get } from "svelte/store" import { ApiVersion } from "constants" /** @@ -28,6 +29,13 @@ const makeApiCall = async ({ method, url, body, json = true }) => { ...(json && { "Content-Type": "application/json" }), ...(!inBuilder && { "x-budibase-type": "client" }), } + + // add csrf token if authenticated + const auth = get(authStore) + if (auth && auth.csrfToken) { + headers["x-csrf-token"] = auth.csrfToken + } + const response = await fetch(url, { method, headers, diff --git a/packages/server/package.json b/packages/server/package.json index ad3618dd55..41278a434e 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "1.0.46-alpha.6", + "version": "1.0.46-alpha.8", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -70,9 +70,9 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "^10.0.3", - "@budibase/backend-core": "^1.0.46-alpha.6", - "@budibase/client": "^1.0.46-alpha.6", - "@budibase/string-templates": "^1.0.46-alpha.6", + "@budibase/backend-core": "^1.0.46-alpha.8", + "@budibase/client": "^1.0.46-alpha.8", + "@budibase/string-templates": "^1.0.46-alpha.8", "@bull-board/api": "^3.7.0", "@bull-board/koa": "^3.7.0", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index 9197fa30a1..e165fd29a5 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -83,12 +83,13 @@ async function getAppUrl(ctx) { if (ctx.request.body.url) { // if the url is provided, use that url = encodeURI(ctx.request.body.url) - } else { + } else if (ctx.request.body.name) { // otherwise use the name url = encodeURI(`${ctx.request.body.name}`) } - url = `/${url.replace(URL_REGEX_SLASH, "")}`.toLowerCase() - + if (url) { + url = `/${url.replace(URL_REGEX_SLASH, "")}`.toLowerCase() + } return url } @@ -278,16 +279,22 @@ exports.create = async ctx => { ctx.body = newApplication } +// This endpoint currently operates as a PATCH rather than a PUT +// Thus name and url fields are handled only if present exports.update = async ctx => { const apps = await getAllApps({ dev: true }) // validation const name = ctx.request.body.name - checkAppName(ctx, apps, name, ctx.params.appId) + if (name) { + checkAppName(ctx, apps, name, ctx.params.appId) + } const url = await getAppUrl(ctx) - checkAppUrl(ctx, apps, url, ctx.params.appId) + if (url) { + checkAppUrl(ctx, apps, url, ctx.params.appId) + ctx.request.body.url = url + } - const appPackageUpdates = { name, url } - const data = await updateAppPackage(appPackageUpdates, ctx.params.appId) + const data = await updateAppPackage(ctx.request.body, ctx.params.appId) ctx.status = 200 ctx.body = data } diff --git a/packages/server/src/api/controllers/auth.js b/packages/server/src/api/controllers/auth.js index b082bb889e..3d89825631 100644 --- a/packages/server/src/api/controllers/auth.js +++ b/packages/server/src/api/controllers/auth.js @@ -15,6 +15,8 @@ exports.fetchSelf = async ctx => { const user = await getFullUser(ctx, userId) // this shouldn't be returned by the app self delete user.roles + // forward the csrf token from the session + user.csrfToken = ctx.user.csrfToken if (getAppId()) { const db = getAppDB() @@ -23,6 +25,8 @@ exports.fetchSelf = async ctx => { try { const userTable = await db.get(InternalTables.USER_METADATA) const metadata = await db.get(userId) + // make sure there is never a stale csrf token + delete metadata.csrfToken // specifically needs to make sure is enriched ctx.body = await outputProcessing(ctx, userTable, { ...user, diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 5524a08bab..208d3a60a3 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -165,6 +165,8 @@ exports.updateSelfMetadata = async function (ctx) { ctx.request.body._id = ctx.user._id // make sure no stale rev delete ctx.request.body._rev + // make sure no csrf token + delete ctx.request.body.csrfToken await exports.updateMetadata(ctx) } diff --git a/packages/server/src/api/routes/tests/auth.spec.js b/packages/server/src/api/routes/tests/auth.spec.js index c50780a8d5..fa26eb83ac 100644 --- a/packages/server/src/api/routes/tests/auth.spec.js +++ b/packages/server/src/api/routes/tests/auth.spec.js @@ -13,10 +13,9 @@ describe("/authenticate", () => { describe("fetch self", () => { it("should be able to fetch self", async () => { - const headers = await config.login() const res = await request .get(`/api/self`) - .set(headers) + .set(config.defaultHeaders()) .expect("Content-Type", /json/) .expect(200) expect(res.body._id).toEqual(generateUserMetadataID("us_uuid1")) diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js index e3414192af..c8d6497ca3 100644 --- a/packages/server/src/middleware/authorized.js +++ b/packages/server/src/middleware/authorized.js @@ -9,11 +9,60 @@ const { } = require("@budibase/backend-core/permissions") const builderMiddleware = require("./builder") const { isWebhookEndpoint } = require("./utils") +const { buildCsrfMiddleware } = require("@budibase/backend-core/auth") +const { getAppId } = require("@budibase/backend-core/context") function hasResource(ctx) { return ctx.resourceId != null } +const csrf = buildCsrfMiddleware() + +/** + * Apply authorization to the requested resource: + * - If this is a builder resource the user must be a builder. + * - Builders can access all resources. + * - Otherwise the user must have the required role. + */ +const checkAuthorized = async (ctx, resourceRoles, permType, permLevel) => { + // check if this is a builder api and the user is not a builder + const isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global + const isBuilderApi = permType === PermissionTypes.BUILDER + if (isBuilderApi && !isBuilder) { + return ctx.throw(403, "Not Authorized") + } + + // check for resource authorization + if (!isBuilder) { + await checkAuthorizedResource(ctx, resourceRoles, permType, permLevel) + } +} + +const checkAuthorizedResource = async ( + ctx, + resourceRoles, + permType, + permLevel +) => { + // get the user's roles + const roleId = ctx.roleId || BUILTIN_ROLE_IDS.PUBLIC + const userRoles = await getUserRoleHierarchy(roleId, { + idOnly: false, + }) + const permError = "User does not have permission" + // check if the user has the required role + if (resourceRoles.length > 0) { + // deny access if the user doesn't have the required resource role + const found = userRoles.find(role => resourceRoles.indexOf(role._id) !== -1) + if (!found) { + ctx.throw(403, permError) + } + // fallback to the base permissions when no resource roles are found + } else if (!doesHaveBasePermission(permType, permLevel, userRoles)) { + ctx.throw(403, permError) + } +} + module.exports = (permType, permLevel = null) => async (ctx, next) => { @@ -31,40 +80,27 @@ module.exports = // to find API endpoints which are builder focused await builderMiddleware(ctx, permType) - const isAuthed = ctx.isAuthenticated - // builders for now have permission to do anything - let isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global - const isBuilderApi = permType === PermissionTypes.BUILDER - if (isBuilder) { + // get the resource roles + let resourceRoles = [] + const appId = getAppId() + if (appId && hasResource(ctx)) { + resourceRoles = await getRequiredResourceRole(permLevel, ctx) + } + + // if the resource is public, proceed + const isPublicResource = resourceRoles.includes(BUILTIN_ROLE_IDS.PUBLIC) + if (isPublicResource) { return next() - } else if (isBuilderApi && !isBuilder) { - return ctx.throw(403, "Not Authorized") } - // need to check this first, in-case public access, don't check authed until last - const roleId = ctx.roleId || BUILTIN_ROLE_IDS.PUBLIC - const hierarchy = await getUserRoleHierarchy(roleId, { - idOnly: false, - }) - const permError = "User does not have permission" - let possibleRoleIds = [] - if (hasResource(ctx)) { - possibleRoleIds = await getRequiredResourceRole(permLevel, ctx) - } - // check if we found a role, if not fallback to base permissions - if (possibleRoleIds.length > 0) { - const found = hierarchy.find( - role => possibleRoleIds.indexOf(role._id) !== -1 - ) - return found ? next() : ctx.throw(403, permError) - } else if (!doesHaveBasePermission(permType, permLevel, hierarchy)) { - ctx.throw(403, permError) + // check authenticated + if (!ctx.isAuthenticated) { + return ctx.throw(403, "Session not authenticated") } - // if they are not authed, then anything using the authorized middleware will fail - if (!isAuthed) { - ctx.throw(403, "Session not authenticated") - } + // check authorized + await checkAuthorized(ctx, resourceRoles, permType, permLevel) - return next() + // csrf protection + return csrf(ctx, next) } diff --git a/packages/server/src/middleware/tests/authorized.spec.js b/packages/server/src/middleware/tests/authorized.spec.js index 205d0b8d2c..9cfa9d368f 100644 --- a/packages/server/src/middleware/tests/authorized.spec.js +++ b/packages/server/src/middleware/tests/authorized.spec.js @@ -20,6 +20,7 @@ class TestConfiguration { this.middleware = authorizedMiddleware(role) this.next = jest.fn() this.throw = jest.fn() + this.headers = {} this.ctx = { headers: {}, request: { @@ -28,7 +29,8 @@ class TestConfiguration { appId: APP_ID, auth: {}, next: this.next, - throw: this.throw + throw: this.throw, + get: (name) => this.headers[name], } } @@ -51,7 +53,7 @@ class TestConfiguration { } setAuthenticated(isAuthed) { - this.ctx.auth = { authenticated: isAuthed } + this.ctx.isAuthenticated = isAuthed } setRequestUrl(url) { @@ -112,7 +114,7 @@ describe("Authorization middleware", () => { expect(config.next).toHaveBeenCalled() }) - it("throws if the user has only builder permissions", async () => { + it("throws if the user does not have builder permissions", async () => { config.setEnvironment(false) config.setMiddlewareRequiredPermission(PermissionTypes.BUILDER) config.setUser({ @@ -138,7 +140,7 @@ describe("Authorization middleware", () => { expect(config.next).toHaveBeenCalled() }) - it("throws if the user session is not authenticated after permission checks", async () => { + it("throws if the user session is not authenticated", async () => { config.setUser({ role: { _id: "" diff --git a/packages/server/src/tests/utilities/TestConfiguration.js b/packages/server/src/tests/utilities/TestConfiguration.js index f08067ea2e..2a0a667792 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.js +++ b/packages/server/src/tests/utilities/TestConfiguration.js @@ -28,6 +28,7 @@ const context = require("@budibase/backend-core/context") const GLOBAL_USER_ID = "us_uuid1" const EMAIL = "babs@babs.com" +const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306" class TestConfiguration { constructor(openServer = true) { @@ -97,7 +98,11 @@ class TestConfiguration { roles: roles || {}, tenantId: TENANT_ID, } - await createASession(id, { sessionId: "sessionid", tenantId: TENANT_ID }) + await createASession(id, { + sessionId: "sessionid", + tenantId: TENANT_ID, + csrfToken: CSRF_TOKEN, + }) if (builder) { user.builder = { global: true } } else { @@ -144,6 +149,7 @@ class TestConfiguration { `${Cookies.Auth}=${authToken}`, `${Cookies.CurrentApp}=${appToken}`, ], + [Headers.CSRF_TOKEN]: CSRF_TOKEN, } if (this.appId) { headers[Headers.APP_ID] = this.appId diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 868553f68d..bde8489166 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "1.0.46-alpha.6", + "version": "1.0.46-alpha.8", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/worker/package.json b/packages/worker/package.json index d3a7a12dc1..85a4080cc3 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "1.0.46-alpha.6", + "version": "1.0.46-alpha.8", "description": "Budibase background service", "main": "src/index.js", "repository": { @@ -29,8 +29,8 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "^1.0.46-alpha.6", - "@budibase/string-templates": "^1.0.46-alpha.6", + "@budibase/backend-core": "^1.0.46-alpha.8", + "@budibase/string-templates": "^1.0.46-alpha.8", "@koa/router": "^8.0.0", "@sentry/node": "^6.0.0", "@techpass/passport-openidconnect": "^0.3.0", diff --git a/packages/worker/src/api/controllers/global/users.js b/packages/worker/src/api/controllers/global/users.js index 676c597b84..f2d89e103a 100644 --- a/packages/worker/src/api/controllers/global/users.js +++ b/packages/worker/src/api/controllers/global/users.js @@ -172,6 +172,7 @@ exports.getSelf = async ctx => { ctx.body.account = ctx.user.account ctx.body.budibaseAccess = ctx.user.budibaseAccess ctx.body.accountPortalAccess = ctx.user.accountPortalAccess + ctx.body.csrfToken = ctx.user.csrfToken } exports.updateSelf = async ctx => { @@ -190,6 +191,8 @@ exports.updateSelf = async ctx => { // don't allow sending up an ID/Rev, always use the existing one delete ctx.request.body._id delete ctx.request.body._rev + // don't allow setting the csrf token + delete ctx.request.body.csrfToken const response = await db.put({ ...user, ...ctx.request.body, diff --git a/packages/worker/src/api/index.js b/packages/worker/src/api/index.js index a83b39e6cf..607d8283f9 100644 --- a/packages/worker/src/api/index.js +++ b/packages/worker/src/api/index.js @@ -6,6 +6,7 @@ const { buildAuthMiddleware, auditLog, buildTenancyMiddleware, + buildCsrfMiddleware, } = require("@budibase/backend-core/auth") const PUBLIC_ENDPOINTS = [ @@ -68,6 +69,10 @@ const NO_TENANCY_ENDPOINTS = [ }, ] +// most public endpoints are gets, but some are posts +// add them all to be safe +const NO_CSRF_ENDPOINTS = [...PUBLIC_ENDPOINTS] + const router = new Router() router .use( @@ -85,6 +90,7 @@ router .use("/health", ctx => (ctx.status = 200)) .use(buildAuthMiddleware(PUBLIC_ENDPOINTS)) .use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS)) + .use(buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS })) // for now no public access is allowed to worker (bar health check) .use((ctx, next) => { if (ctx.publicEndpoint) { diff --git a/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js b/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js index 34ce01263d..6b6c0e24b3 100644 --- a/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js +++ b/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js @@ -2,12 +2,12 @@ const env = require("../../../../environment") const controllers = require("./controllers") const supertest = require("supertest") const { jwt } = require("@budibase/backend-core/auth") -const { Cookies } = require("@budibase/backend-core/constants") +const { Cookies, Headers } = require("@budibase/backend-core/constants") const { Configs, LOGO_URL } = require("../../../../constants") const { getGlobalUserByEmail } = require("@budibase/backend-core/utils") const { createASession } = require("@budibase/backend-core/sessions") const { newid } = require("@budibase/backend-core/src/hashing") -const { TENANT_ID } = require("./structures") +const { TENANT_ID, CSRF_TOKEN } = require("./structures") const core = require("@budibase/backend-core") const CouchDB = require("../../../../db") const { doInTenant } = require("@budibase/backend-core/tenancy") @@ -72,6 +72,7 @@ class TestConfiguration { await createASession("us_uuid1", { sessionId: "sessionid", tenantId: TENANT_ID, + csrfToken: CSRF_TOKEN, }) } @@ -98,6 +99,7 @@ class TestConfiguration { return { Accept: "application/json", ...this.cookieHeader([`${Cookies.Auth}=${authToken}`]), + [Headers.CSRF_TOKEN]: CSRF_TOKEN, } } diff --git a/packages/worker/src/api/routes/tests/utilities/structures.js b/packages/worker/src/api/routes/tests/utilities/structures.js index 16701ac3d7..45f1f0077c 100644 --- a/packages/worker/src/api/routes/tests/utilities/structures.js +++ b/packages/worker/src/api/routes/tests/utilities/structures.js @@ -1 +1,2 @@ exports.TENANT_ID = "default" +exports.CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"