Merge pull request #12684 from Budibase/vulnerability/budi-7793-ddos
Set password limits
This commit is contained in:
commit
9e33e53caa
|
@ -1 +1 @@
|
||||||
Subproject commit c1a53bb2f4cafcb4c55ad7181146617b449907f2
|
Subproject commit aca3c9b6b5170d35a255ceb89e57a21719f5ed29
|
|
@ -33,6 +33,7 @@ export * as docUpdates from "./docUpdates"
|
||||||
export * from "./utils/Duration"
|
export * from "./utils/Duration"
|
||||||
export { SearchParams } from "./db"
|
export { SearchParams } from "./db"
|
||||||
export * as docIds from "./docIds"
|
export * as docIds from "./docIds"
|
||||||
|
export * as security from "./security"
|
||||||
// Add context to tenancy for backwards compatibility
|
// Add context to tenancy for backwards compatibility
|
||||||
// only do this for external usages to prevent internal
|
// only do this for external usages to prevent internal
|
||||||
// circular dependencies
|
// circular dependencies
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { env } from ".."
|
||||||
|
|
||||||
|
export const PASSWORD_MIN_LENGTH = +(process.env.PASSWORD_MIN_LENGTH || 8)
|
||||||
|
export const PASSWORD_MAX_LENGTH = +(process.env.PASSWORD_MAX_LENGTH || 512)
|
||||||
|
|
||||||
|
export function validatePassword(
|
||||||
|
password: string
|
||||||
|
): { valid: true } | { valid: false; error: string } {
|
||||||
|
if (!password || password.length < PASSWORD_MIN_LENGTH) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `Password invalid. Minimum ${PASSWORD_MIN_LENGTH} characters.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length > PASSWORD_MAX_LENGTH) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `Password invalid. Maximum ${PASSWORD_MAX_LENGTH} characters.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true }
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./auth"
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { generator } from "../../../tests"
|
||||||
|
import { PASSWORD_MAX_LENGTH, validatePassword } from "../auth"
|
||||||
|
|
||||||
|
describe("auth", () => {
|
||||||
|
describe("validatePassword", () => {
|
||||||
|
it("a valid password returns successful", () => {
|
||||||
|
expect(validatePassword("password")).toEqual({ valid: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["undefined", undefined],
|
||||||
|
["null", null],
|
||||||
|
["empty", ""],
|
||||||
|
])("%s returns unsuccessful", (_, password) => {
|
||||||
|
expect(validatePassword(password as string)).toEqual({
|
||||||
|
valid: false,
|
||||||
|
error: "Password invalid. Minimum 8 characters.",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
generator.word({ length: PASSWORD_MAX_LENGTH }),
|
||||||
|
generator.paragraph().substring(0, PASSWORD_MAX_LENGTH),
|
||||||
|
])(`can use passwords up to 512 characters in length`, password => {
|
||||||
|
expect(validatePassword(password)).toEqual({
|
||||||
|
valid: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
generator.word({ length: PASSWORD_MAX_LENGTH + 1 }),
|
||||||
|
generator
|
||||||
|
.paragraph({ sentences: 50 })
|
||||||
|
.substring(0, PASSWORD_MAX_LENGTH + 1),
|
||||||
|
])(
|
||||||
|
`passwords cannot have more than ${PASSWORD_MAX_LENGTH} characters`,
|
||||||
|
password => {
|
||||||
|
expect(validatePassword(password)).toEqual({
|
||||||
|
valid: false,
|
||||||
|
error: "Password invalid. Maximum 512 characters.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
|
@ -27,6 +27,7 @@ import {
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
import { searchExistingEmails } from "./lookup"
|
import { searchExistingEmails } from "./lookup"
|
||||||
import { hash } from "../utils"
|
import { hash } from "../utils"
|
||||||
|
import { validatePassword } from "../security"
|
||||||
|
|
||||||
type QuotaUpdateFn = (
|
type QuotaUpdateFn = (
|
||||||
change: number,
|
change: number,
|
||||||
|
@ -110,6 +111,12 @@ export class UserDB {
|
||||||
if (await UserDB.isPreventPasswordActions(user, account)) {
|
if (await UserDB.isPreventPasswordActions(user, account)) {
|
||||||
throw new HTTPError("Password change is disabled for this user", 400)
|
throw new HTTPError("Password change is disabled for this user", 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const passwordValidation = validatePassword(password)
|
||||||
|
if (!passwordValidation.valid) {
|
||||||
|
throw new HTTPError(passwordValidation.error, 400)
|
||||||
|
}
|
||||||
|
|
||||||
hashedPassword = opts.hashPassword ? await hash(password) : password
|
hashedPassword = opts.hashPassword ? await hash(password) : password
|
||||||
} else if (dbUser) {
|
} else if (dbUser) {
|
||||||
hashedPassword = dbUser.password
|
hashedPassword = dbUser.password
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
|
||||||
_id: userId,
|
_id: userId,
|
||||||
userId,
|
userId,
|
||||||
email: newEmail(),
|
email: newEmail(),
|
||||||
password: "test",
|
password: "password",
|
||||||
roles: { app_test: "admin" },
|
roles: { app_test: "admin" },
|
||||||
firstName: generator.first(),
|
firstName: generator.first(),
|
||||||
lastName: generator.last(),
|
lastName: generator.last(),
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
$goto("../portal")
|
$goto("../portal")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
submitted = false
|
submitted = false
|
||||||
notifications.error("Failed to create admin user")
|
notifications.error(error.message || "Failed to create admin user")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
submitted = false
|
submitted = false
|
||||||
notifications.error("Unable to reset password")
|
notifications.error(err.message || "Unable to reset password")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ async function init() {
|
||||||
HTTP_MIGRATIONS: "0",
|
HTTP_MIGRATIONS: "0",
|
||||||
HTTP_LOGGING: "0",
|
HTTP_LOGGING: "0",
|
||||||
VERSION: "0.0.0+local",
|
VERSION: "0.0.0+local",
|
||||||
|
PASSWORD_MIN_LENGTH: "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
config = { ...config, ...existingConfig }
|
config = { ...config, ...existingConfig }
|
||||||
|
|
|
@ -30,6 +30,7 @@ async function init() {
|
||||||
ENABLE_EMAIL_TEST_MODE: "1",
|
ENABLE_EMAIL_TEST_MODE: "1",
|
||||||
HTTP_LOGGING: "0",
|
HTTP_LOGGING: "0",
|
||||||
VERSION: "0.0.0+local",
|
VERSION: "0.0.0+local",
|
||||||
|
PASSWORD_MIN_LENGTH: "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
config = { ...config, ...existingConfig }
|
config = { ...config, ...existingConfig }
|
||||||
|
|
|
@ -122,10 +122,10 @@ export const resetUpdate = async (ctx: Ctx<PasswordResetUpdateRequest>) => {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: "password reset successfully.",
|
message: "password reset successfully.",
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.warn(err)
|
console.warn(err)
|
||||||
// hide any details of the error for security
|
// hide any details of the error for security
|
||||||
ctx.throw(400, "Cannot reset password.")
|
ctx.throw(400, err.message || "Cannot reset password.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -229,7 +229,7 @@ describe("/api/global/auth", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(res.body).toEqual({
|
expect(res.body).toEqual({
|
||||||
message: "Cannot reset password.",
|
message: "Password change is disabled for this user",
|
||||||
status: 400,
|
status: 400,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -261,8 +261,12 @@ describe("/api/global/auth", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
// convert to account owner now that password has been requested
|
// convert to account owner now that password has been requested
|
||||||
const account = structures.accounts.ssoAccount() as CloudAccount
|
const account: CloudAccount = {
|
||||||
mocks.accounts.getAccount.mockReturnValueOnce(
|
...structures.accounts.ssoAccount(),
|
||||||
|
budibaseUserId: "budibaseUserId",
|
||||||
|
email: user.email,
|
||||||
|
}
|
||||||
|
mocks.accounts.getAccountByTenantId.mockReturnValueOnce(
|
||||||
Promise.resolve(account)
|
Promise.resolve(account)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ class TestConfiguration {
|
||||||
tenantId: string
|
tenantId: string
|
||||||
user?: User
|
user?: User
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
userPassword = "test"
|
userPassword = "password"
|
||||||
|
|
||||||
constructor(opts: { openServer: boolean } = { openServer: true }) {
|
constructor(opts: { openServer: boolean } = { openServer: true }) {
|
||||||
// default to cloud hosting
|
// default to cloud hosting
|
||||||
|
|
|
@ -101,7 +101,7 @@ export class UserAPI extends TestAPI {
|
||||||
if (!request) {
|
if (!request) {
|
||||||
request = {
|
request = {
|
||||||
email: structures.email(),
|
email: structures.email(),
|
||||||
password: generator.string(),
|
password: generator.string({ length: 8 }),
|
||||||
tenantId: structures.tenant.id(),
|
tenantId: structures.tenant.id(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue