Skip to content

Commit 246124a

Browse files
committed
Add “refreshTimeoutOnRequest” option, as well as a minor bug fix.
1 parent 04aa283 commit 246124a

8 files changed

Lines changed: 97 additions & 20 deletions

File tree

README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ Classes
3232
### ExpressBrute(store, options)
3333
- `store` An instance of `ExpressBrute.MemoryStore` or `ExpressBrute.MemcachedStore`
3434
- `options`
35-
- `freeRetries` The number of retires the user has before they need to start waiting (default: 2)
36-
- `minWait` The initial wait time (in milliseconds) after the user runs out of retries (default: 500 milliseconds)
37-
- `maxWait` The maximum amount of time (in milliseconds) between requests the user needs to wait (default: 15 minutes). The wait for a given request is determined by adding the time the user needed to wait for the previous two requests.
38-
- `lifetime` The length of time (in seconds since the last request) to remember the number of requests that have been made by an IP. By default it will be set to `maxWait * the number of attempts before you hit maxWait` to discourage simply waiting for the lifetime to expire before resuming an attack. With default values this is about 6 hours.
35+
- `freeRetries` The number of retires the user has before they need to start waiting (default: 2)
36+
- `minWait` The initial wait time (in milliseconds) after the user runs out of retries (default: 500 milliseconds)
37+
- `maxWait` The maximum amount of time (in milliseconds) between requests the user needs to wait (default: 15 minutes). The wait for a given request is determined by adding the time the user needed to wait for the previous two requests.
38+
- `lifetime` The length of time (in seconds since the last request) to remember the number of requests that have been made by an IP. By default it will be set to `maxWait * the number of attempts before you hit maxWait` to discourage simply waiting for the lifetime to expire before resuming an attack. With default values this is about 6 hours.
3939
- `failCallback` gets called with (`req`, `resp`, `next`, `nextValidRequestDate`) when a request is rejected (default: ExpressBrute.FailForbidden)
40-
- `proxyDepth` Specifies how many levels of the `X-Forwarded-For` header to trust. If your web server is behind a CDN and/or load balancer you'll need to set this to however many levels of proxying it's behind to get a valid IP. Setting this too high allows attackers to get around brute force protection by spoofing the `X-Forwarded-For` header, so don't set it higher than you need to (default: 0)
41-
- `attachResetToRequest` Specify whether or not a simplified reset method should be attached at `req.brute.reset`. The simplified method takes only a callback, and resets all `ExpressBrute` middleware that was called on the current request. If multiple instances of `ExpressBrute` have middleware on the same request, only those with `attachResetToRequest` set to true will be reset (default: true)
40+
- `proxyDepth` Specifies how many levels of the `X-Forwarded-For` header to trust. If your web server is behind a CDN and/or load balancer you'll need to set this to however many levels of proxying it's behind to get a valid IP. Setting this too high allows attackers to get around brute force protection by spoofing the `X-Forwarded-For` header, so don't set it higher than you need to (default: 0)
41+
- `attachResetToRequest` Specify whether or not a simplified reset method should be attached at `req.brute.reset`. The simplified method takes only a callback, and resets all `ExpressBrute` middleware that was called on the current request. If multiple instances of `ExpressBrute` have middleware on the same request, only those with `attachResetToRequest` set to true will be reset (default: true)
42+
- `refreshTimeoutOnRequest` Defines whether the remaining `lifetime` of a counter should be based on the time since the last request (true) of the time since the first request (false). Useful for allowing limits over fixed periods of time, for example a limited number of requests per day. (Default: true)
4243

4344
### ExpressBrute.MemoryStore()
4445
An in-memory store for persisting request counts. Don't use this in production.
@@ -111,6 +112,7 @@ var globalBruteforce = new ExpressBrute(store, {
111112
freeRetries: 1000,
112113
proxyDepth: 1,
113114
attachResetToRequest: false,
115+
refreshTimeoutOnRequest: false,
114116
winWait: 25*60*60*1000, // 1 day 1 hour (should never reach this wait time)
115117
maxWait: 25*60*60*1000, // 1 day 1 hour (should never reach this wait time)
116118
lifetime: 24*60*60*1000, // 1 day
@@ -141,6 +143,10 @@ app.post('/auth',
141143

142144
Changelog
143145
---------
146+
### v0.4.1
147+
* NEW: `refreshTimeoutOnRequest` option that allows you to prevent the remaining `lifetime` for a timer from being reset on each request (useful for implementing limits for set time frames, e.g. requests per day)
148+
* BUG: Lifetimes were not previously getting extended properly for instances of `ExpressBrute.MemoryStore`
149+
144150
### v0.4.0
145151
* NEW: `attachResetToRequest` parameter that lets you prevent the request object being decorated
146152
* NEW: `failCallback` can be overriden by `getMiddleware`

index.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,12 @@ ExpressBrute.prototype.getMiddleware = function (options) {
8686

8787
var count = 0,
8888
delay = 0,
89-
lastValidRequestTime = this.now();
89+
lastValidRequestTime = this.now(),
90+
firstRequestTime = lastValidRequestTime;
9091
if (value) {
9192
count = value.count;
9293
lastValidRequestTime = value.lastRequest.getTime();
94+
firstRequestTime = value.firstRequest.getTime();
9395

9496
var delayIndex = value.count - this.options.freeRetries - 1;
9597
if (delayIndex >= 0) {
@@ -100,10 +102,26 @@ ExpressBrute.prototype.getMiddleware = function (options) {
100102
}
101103
}
102104
}
103-
var nextValidRequestTime = lastValidRequestTime+delay;
104-
105+
var nextValidRequestTime = lastValidRequestTime+delay,
106+
remainingLifetime = this.options.lifetime || 0;
107+
108+
if (!this.options.refreshTimeoutOnRequest && remainingLifetime > 0) {
109+
remainingLifetime = remainingLifetime - Math.floor((this.now() - firstRequestTime) / 1000);
110+
if (remainingLifetime < 1) {
111+
// it should be expired alredy, treat this as a new request and reset everything
112+
count = 0;
113+
delay = 0;
114+
nextValidRequestTime = firstRequestTime = lastValidRequestTime = this.now();
115+
remainingLifetime = this.options.lifetime || 0;
116+
}
117+
}
118+
105119
if (nextValidRequestTime <= this.now()) {
106-
this.store.set(key, {count: count+1, lastRequest: new Date(this.now())}, this.options.lifetime || 0, function (err) {
120+
this.store.set(key, {
121+
count: count+1,
122+
lastRequest: new Date(this.now()),
123+
firstRequest: new Date(firstRequestTime)
124+
}, remainingLifetime, function (err) {
107125
if (err) {
108126
throw "Cannot increment request count";
109127
}
@@ -150,6 +168,7 @@ ExpressBrute.defaults = {
150168
freeRetries: 2,
151169
proxyDepth: 0,
152170
attachResetToRequest: true,
171+
refreshTimeoutOnRequest: true,
153172
minWait: 500,
154173
maxWait: 1000*60*15, // 15 minutes
155174
failCallback: ExpressBrute.FailForbidden

lib/AbstractClientStore.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ AbstractClientStore.prototype.increment = function (key, lifetime, callback) {
88
callback(err);
99
} else {
1010
var count = value ? value.count+1 : 1;
11-
self.set(key, {count: count, lastRequest: new Date()}, lifetime, function (err) {
11+
self.set(key, {count: count, lastRequest: new Date(), firstRequest: new Date()}, lifetime, function (err) {
1212
var prevValue = {
1313
count: value ? value.count : 0,
14-
lastRequest: value ? value.lastRequest : null
14+
lastRequest: value ? value.lastRequest : null,
15+
firstRequest: value ? value.firstRequest : null
1516
};
1617
typeof callback == 'function' && callback(err, prevValue);
1718
});

lib/MemcachedStore.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ MemcachedStore.prototype.get = function (key, callback) {
2323
if (data) {
2424
data = JSON.parse(data);
2525
data.lastRequest = new Date(data.lastRequest);
26+
data.firstRequest = new Date(data.firstRequest);
2627
}
2728
typeof callback == 'function' && callback(err, data);
2829
}

mock/MemcachedMock.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ MemcachedMock.prototype.set = function (key, value, lifetime, callback) {
1010
} else if (this.data[key].timeout) {
1111
clearTimeout(this.data[key].timeout);
1212
}
13-
this.data[key] = value;
13+
this.data[key].value = value;
1414

1515
if (lifetime) {
1616
this.data[key].timeout = setTimeout(_.bind(function () {
@@ -20,7 +20,7 @@ MemcachedMock.prototype.set = function (key, value, lifetime, callback) {
2020
typeof callback == 'function' && callback(null);
2121
};
2222
MemcachedMock.prototype.get = function (key, callback) {
23-
typeof callback == 'function' && callback(null, this.data[key]);
23+
typeof callback == 'function' && callback(null, this.data[key] && this.data[key].value);
2424
};
2525
MemcachedMock.prototype.del = function (key, callback) {
2626
if (this.data[key] && this.data[key].timeout) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "express-brute",
3-
"version": "0.4.0",
3+
"version": "0.4.1",
44
"description": "A brute-force protection middleware for express routes that rate limits incoming requests",
55
"keywords": [
66
"brute",

spec/ExpessBrute.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,55 @@ describe("express brute", function () {
170170
expect(errorSpy.calls.length).toEqual(1);
171171
});
172172
});
173+
it("doesn't extend the lifetime if refreshTimeoutOnRequest is false", function () {
174+
brute = new ExpressBrute(store, {
175+
freeRetries: 0,
176+
minWait: 10000,
177+
maxWait: 10000,
178+
lifetime: 1,
179+
refreshTimeoutOnRequest: false,
180+
failCallback: errorSpy
181+
});
182+
runs(function () {
183+
brute.prevent(req(), new ResponseMock(), nextSpy);
184+
expect(errorSpy).not.toHaveBeenCalled();
185+
brute.prevent(req(), new ResponseMock(), nextSpy);
186+
expect(errorSpy).toHaveBeenCalled();
187+
});
188+
waits((brute.options.lifetime*500));
189+
runs(function () {
190+
brute.prevent(req(), new ResponseMock(), nextSpy);
191+
expect(errorSpy.calls.length).toEqual(2);
192+
});
193+
waits((brute.options.lifetime*500)+1);
194+
runs(function () {
195+
brute.prevent(req(), new ResponseMock(), nextSpy);
196+
expect(errorSpy.calls.length).toEqual(2);
197+
});
198+
});
199+
it('does extend the lifetime if refreshTimeoutOnRequest is true', function () {
200+
brute = new ExpressBrute(store, {
201+
freeRetries: 1,
202+
minWait: 10000,
203+
maxWait: 10000,
204+
lifetime: 1,
205+
failCallback: errorSpy
206+
});
207+
runs(function () {
208+
brute.prevent(req(), new ResponseMock(), nextSpy);
209+
expect(errorSpy).not.toHaveBeenCalled();
210+
});
211+
waits((brute.options.lifetime*500));
212+
runs(function () {
213+
brute.prevent(req(), new ResponseMock(), nextSpy);
214+
expect(errorSpy).not.toHaveBeenCalled();
215+
});
216+
waits((brute.options.lifetime*500)+1);
217+
runs(function () {
218+
brute.prevent(req(), new ResponseMock(), nextSpy);
219+
expect(errorSpy).toHaveBeenCalled();
220+
});
221+
});
173222
it('allows failCallback to be overridden', function () {
174223
brute = new ExpressBrute(store, {
175224
freeRetries: 0,

spec/MemcachedStore.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe("Express brute memcached store", function () {
1515
});
1616
it("can set a key and get it back", function () {
1717
var curDate = new Date(),
18-
object = {count: 1, lastRequest: curDate};
18+
object = {count: 1, lastRequest: curDate, firstRequest: curDate};
1919
runs(function () {
2020
instance.set("1.2.3.4", object, 0, callback);
2121
});
@@ -37,7 +37,7 @@ describe("Express brute memcached store", function () {
3737
});
3838
it("increments values and returns that last value", function () {
3939
var curDate = new Date(),
40-
object = {count: 1, lastRequest: curDate};
40+
object = {count: 1, lastRequest: curDate, firstRequest: curDate};
4141
runs(function () {
4242
instance.set("1.2.3.4", object, 0, callback);
4343
});
@@ -75,7 +75,7 @@ describe("Express brute memcached store", function () {
7575

7676
runs(function () {
7777
expect(callback.mostRecentCall.args[0]).toBe(null);
78-
expect(callback.mostRecentCall.args[1]).toEqual({count: 0, lastRequest: null});
78+
expect(callback.mostRecentCall.args[1]).toEqual({count: 0, lastRequest: null, firstRequest: null});
7979

8080
instance.get("1.2.3.4", callback);
8181
});
@@ -86,6 +86,7 @@ describe("Express brute memcached store", function () {
8686
expect(callback.mostRecentCall.args[0]).toBe(null);
8787
expect(callback.mostRecentCall.args[1].count).toEqual(1);
8888
expect(callback.mostRecentCall.args[1].lastRequest instanceof Date).toBeTruthy();
89+
expect(callback.mostRecentCall.args[1].firstRequest instanceof Date).toBeTruthy();
8990
});
9091
});
9192
it("returns null when no value is available", function () {
@@ -102,7 +103,7 @@ describe("Express brute memcached store", function () {
102103
});
103104
it("can reset the count of requests", function () {
104105
var curDate = new Date(),
105-
object = {count: 1, lastRequest: curDate};
106+
object = {count: 1, lastRequest: curDate, firstRequest: curDate};
106107
runs(function () {
107108
instance.set("1.2.3.4", object, 0, callback);
108109
});
@@ -141,7 +142,7 @@ describe("Express brute memcached store", function () {
141142
});
142143
it("supports data expiring", function () {
143144
var curDate = new Date(),
144-
object = {count: 1, lastRequest: curDate};
145+
object = {count: 1, lastRequest: curDate, firstRequest: curDate};
145146

146147
runs(function () {
147148
instance.set("1.2.3.4", object, 1, callback);

0 commit comments

Comments
 (0)