-
-
Notifications
You must be signed in to change notification settings - Fork 57
Bypass few common security attacks to Donut Server #146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Changes from all commits
86461c8
a16d679
b2025ec
b1b001c
114655a
9b73bdc
7b2a638
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ const cookieParser = require('cookie-parser') | |
const createError = require('http-errors') | ||
const path = require('path') | ||
const socket = require('socket.io') | ||
const helmet = require('helmet') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using helmet to add few security response headers as it has got standard implementations and widely used in express apps |
||
|
||
const indexRouter = require('./app/routes/index') | ||
const authRouter = require('./app/routes/auth') | ||
|
@@ -40,10 +41,33 @@ app.use(express.urlencoded({ extended: false })) | |
app.use(cookieParser()) | ||
app.use(express.static(path.join(__dirname, 'public'))) | ||
app.use((req, res, next) => { | ||
res.setHeader('Cache-Control', 'no-store') | ||
res.setHeader('Expires', 0) | ||
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does not allow browsers to cache Donut server responses, so using will help to bypass attacks such as CSRF. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For csrf attack you can use this, https://www.npmjs.com/package/csrf There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Rupeshiya I will work on csrf there are a couple of things to do 😅 will add them in my next or There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah sure! |
||
req.io = io | ||
next() | ||
}) | ||
|
||
// setting headers for security | ||
app.disable('x-powered-by') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hide from attackers on what tech stack does the Donut server runs on. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kmehant I think it will only disable express.js to come into view of attackers |
||
app.use(helmet.xssFilter()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bypasses reflected XSS |
||
app.use(helmet.referrerPolicy({ policy: 'same-origin' })) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Privacy] referrer information is shared only within our server. |
||
app.use(helmet.noSniff()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. avoids Browsers from running unrecognized malicious code. |
||
app.use(helmet.frameguard({ action: 'sameorigin' })) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does not allow Donut Server being used in a frame thereby bypasses clickjacking in browsers. |
||
app.use(helmet.hsts({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To make browsers to keep Donut server users on HTTPS protocol strictly There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also will this work without SSL ?? |
||
maxAge: 5184000, | ||
includeSubDomains: true | ||
})) | ||
app.use(helmet.dnsPrefetchControl()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Privacy] Does not allow browsers from preloading the Donut server even before user routes to it. |
||
app.use(helmet.contentSecurityPolicy({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Response header to restrict Browsers in allowing contents only from trusted sources. |
||
directives: { | ||
defaultSrc: ["'self'"], | ||
baseUri: ["'self'"] | ||
// other sources have to mentioned here | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Rupeshiya We might need to add more external sources which are being used in Donut Server if any |
||
}, | ||
browserSniff: false | ||
})) | ||
app.use(helmet.ieNoOpen()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specifically for IE, |
||
|
||
app.use('/notification', notificationRouter) | ||
app.use('/', indexRouter) | ||
app.use('/auth', authRouter) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Util for limiting the number of login retries on using email and password |
||
const Redis = require('../../config/redis') | ||
const { RateLimiterRedis } = require('rate-limiter-flexible') | ||
|
||
const redisClient = Redis.redisClient | ||
|
||
const loginLimiter = new RateLimiterRedis({ | ||
storeClient: redisClient, | ||
keyPrefix: 'login', | ||
points: 5, | ||
duration: 60 * 30 // 30 minutes wait time | ||
}) | ||
Comment on lines
+7
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. config for rate limiting
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kmehant You are rate-limiting only for the login endpoints so DOS attack is still possible by a logged-in user.
This will hit the server after every 1second and due to infinite hit server will go down. Please take it into consideration. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Rupeshiya Yes, this point is noted ✌🏻. So in our next pr, I will simply write a middleware that will keep track of the number of requests (immediate) and IP address of the Client, and then we will use a quota system where we will block the user from accessing the service. (this truly comes under rate-limiting 😅 and not the present work, sorry if I have used the word rate-limiting anywhere in the context for the present work.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes!! |
||
|
||
const limiterConsecutiveOutOfLimits = new RateLimiterRedis({ | ||
storeClient: redisClient, | ||
keyPrefix: 'login_consecutive_outoflimits', | ||
points: 99999, | ||
duration: 0 | ||
}) | ||
Comment on lines
+14
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. config for repeating the process of being out of retries |
||
|
||
function getFibonacciBlockDurationMinutes (countConsecutiveOutOfLimits) { | ||
if (countConsecutiveOutOfLimits <= 1) { | ||
return 1 | ||
} | ||
|
||
return getFibonacciBlockDurationMinutes(countConsecutiveOutOfLimits - 1) + getFibonacciBlockDurationMinutes(countConsecutiveOutOfLimits - 2) | ||
} | ||
|
||
module.exports = { | ||
// restrict brute for attacks on login | ||
rateLimit: async (email, isLoggedIn) => { | ||
const redisUserID = await loginLimiter.get(email) | ||
let retrySecs = 0 | ||
if (isLoggedIn) { | ||
await limiterConsecutiveOutOfLimits.delete(email) | ||
return ('reset done') | ||
} | ||
if (redisUserID !== null && redisUserID.remainingPoints <= 0) { | ||
retrySecs = Math.round(redisUserID.msBeforeNext / 1000) || 1 | ||
} | ||
if (retrySecs > 0) { | ||
return ('Retry after: '.concat(String(retrySecs))) | ||
} else { | ||
try { | ||
const resConsume = await loginLimiter.consume(email) | ||
if (resConsume.remainingPoints <= 0) { | ||
const resPenalty = await limiterConsecutiveOutOfLimits.penalty(email) | ||
await loginLimiter.block(email, 60 * getFibonacciBlockDurationMinutes(resPenalty.consumedPoints)) | ||
} else { | ||
return ('admit') | ||
} | ||
} catch (rlRejected) { | ||
if (rlRejected instanceof Error) { | ||
throw rlRejected | ||
} else { | ||
return ('Too many tries, Retry after'.concat(String(Math.round(rlRejected.msBeforeNext / 1000)) || 1)) | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
const Redis = require('ioredis') | ||
const redisClient = new Redis({ | ||
port: process.env.REDIS_PORT, | ||
host: process.env.REDIS_HOST, | ||
family: 4, | ||
password: process.env.REDIS_PASSWORD, | ||
db: process.env.REDIS_DB | ||
}) | ||
|
||
exports.redisClient = redisClient |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ services: | |
server: | ||
container_name: server | ||
restart: always | ||
expose: | ||
expose: | ||
- "5000" | ||
build: | ||
context: . | ||
|
@@ -23,5 +23,14 @@ services: | |
- db-data:/data/db | ||
ports: | ||
- "27017:27017" | ||
redis: | ||
container_name: redis | ||
image: redis:5-alpine | ||
ports: | ||
- "6379:6379" | ||
volumes: | ||
- redis-data:/data | ||
command: redis-server --requirepass auth | ||
Comment on lines
+26
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added Redis to the container setup |
||
volumes: | ||
db-data: | ||
redis-data: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added Redis connection credentials here