1
0
mirror of https://github.com/sasjs/server.git synced 2026-04-10 07:33:13 +00:00

feat: prevent brute force attack by rate limiting login endpoint

This commit is contained in:
2023-03-28 21:43:10 +05:00
parent c4066d32a0
commit a82cabb001
13 changed files with 286 additions and 16 deletions

View File

@@ -3,7 +3,9 @@ import { seedDB } from './seedDB'
export const connectDB = async () => {
try {
await mongoose.connect(process.env.DB_CONNECT as string)
process.dbInstance = await mongoose.connect(
process.env.DB_CONNECT as string
)
} catch (err) {
throw new Error('Unable to connect to DB!')
}

View File

@@ -0,0 +1,26 @@
import { RateLimiterMongo } from 'rate-limiter-flexible'
export const getRateLimiters = () => {
const {
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY,
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
} = process.env
const limiterSlowBruteByIP = new RateLimiterMongo({
storeClient: process.dbInstance.connection,
keyPrefix: 'login_fail_ip_per_day',
points: Number(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY),
duration: 60 * 60 * 24,
blockDuration: 60 * 60 * 24 // Block for 1 day
})
const limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMongo({
storeClient: process.dbInstance.connection,
keyPrefix: 'login_fail_consecutive_username_and_ip',
points: Number(MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP),
duration: 60 * 60 * 24 * 90, // Store number for 90 days since first fail
blockDuration: 60 * 60 // Block for 1 hour
})
return { limiterSlowBruteByIP, limiterConsecutiveFailsByUsernameAndIP }
}

View File

@@ -13,6 +13,7 @@ export * from './getAuthorizedRoutes'
export * from './getCertificates'
export * from './getDesktopFields'
export * from './getPreProgramVariables'
export * from './getRateLimiters'
export * from './getRunTimeAndFilePath'
export * from './getServerUrl'
export * from './getTokensFromDB'
@@ -20,10 +21,10 @@ export * from './instantiateLogger'
export * from './isDebugOn'
export * from './isPublicRoute'
export * from './ldapClient'
export * from './zipped'
export * from './parseLogToArray'
export * from './removeTokensInDB'
export * from './saveTokensInDB'
export * from './secondsToHms'
export * from './seedDB'
export * from './setProcessVariables'
export * from './setupFolders'
@@ -32,3 +33,4 @@ export * from './upload'
export * from './validation'
export * from './verifyEnvVariables'
export * from './verifyTokenInDB'
export * from './zipped'

View File

@@ -0,0 +1,10 @@
export const secondsToHms = (seconds: number) => {
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
const s = Math.floor((seconds % 3600) % 60)
const hDisplay = h > 0 ? h + (h == 1 ? ' hour, ' : ' hours, ') : ''
const mDisplay = m > 0 ? m + (m == 1 ? ' minute, ' : ' minutes, ') : ''
const sDisplay = s > 0 ? s + (s == 1 ? ' second' : ' seconds') : ''
return hDisplay + mDisplay + sDisplay
}

View File

@@ -77,6 +77,8 @@ export const verifyEnvVariables = (): ReturnCode => {
errors.push(...verifyDbType())
errors.push(...verifyRateLimiter())
if (errors.length) {
process.logger?.error(
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
@@ -367,6 +369,50 @@ const verifyDbType = () => {
return errors
}
const verifyRateLimiter = () => {
const errors: string[] = []
const {
MODE,
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY,
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
} = process.env
if (MODE === ModeType.Server) {
if (MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) {
if (
!isNumeric(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) ||
Number(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) < 1
) {
errors.push(
`- Invalid value for 'MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY' - Only positive number is acceptable`
)
}
} else {
process.env.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY =
DEFAULTS.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY
}
if (MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) {
if (
!isNumeric(MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) ||
Number(MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) < 1
) {
errors.push(
`- Invalid value for 'MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP' - Only positive number is acceptable`
)
}
} else {
process.env.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP =
DEFAULTS.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
}
}
return errors
}
const isNumeric = (val: string): boolean => {
return !isNaN(Number(val))
}
const DEFAULTS = {
MODE: ModeType.Desktop,
PROTOCOL: ProtocolType.HTTP,
@@ -374,5 +420,7 @@ const DEFAULTS = {
HELMET_COEP: HelmetCoepType.TRUE,
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
RUN_TIMES: RunTimeType.SAS,
DB_TYPE: DatabaseType.MONGO
DB_TYPE: DatabaseType.MONGO,
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY: '100',
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP: '10'
}