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:
@@ -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!')
|
||||
}
|
||||
|
||||
26
api/src/utils/getRateLimiters.ts
Normal file
26
api/src/utils/getRateLimiters.ts
Normal 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 }
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
10
api/src/utils/secondsToHms.ts
Normal file
10
api/src/utils/secondsToHms.ts
Normal 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
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user