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"