Languages
[Edit]
EN

JavaScript - prevent multiple login action calling in shot time

6 points
Created by:
Palpys
464

In this article, we're going to have a look at how to prevent in JavaScript multiple actions calling in very shots time.

Presented code shows how to do not let user to make login action (it can be used to prevent any actions) to many times in some short time window. Presented solution keeps infomrations in local storage to prevent state reset on browser refresh action. In many cases of web attacks it is enought to filter first amount of potential attacks on web browser level.

Main idea of presented solution is based on time window. We keep number of actions executed in same second in one array item. When current second elapsed, next array item changes status to currently active (for 10s we use array with 10 items what was presented on the example screenshot). Old items are overridden by new one - itemIndex % arraySize formula is used to calculate indexes to do not remove or append items.

Number of login actions per last 10 seconds keeped in localStorage - JavaScript
Number of login actions per last 10 seconds keeped in localStorage - JavaScript

Run following code to see result:

// ONLINE-RUNNER:browser;

function createArray(count) {
	var array = Array(count);
	for (var i = 0; i < count; ++i) {
		array[i] = 0;
	}
	return array;
}

function resizeArray(array, size) {
	var changeSize = size - array.length;
	if (changeSize > 0) {
		return array.concat(createArray(changeSize));
	} else {
		return array.slice(0, size);
	}
}

function sumValues(array) {
	var sum = 0;
	for (var i = 0; i < array.length; ++i) {
		sum += array[i];
	}
	return sum;
}

function extractSeconds(milliseconds) {
	return Math.floor(0.001 * milliseconds);
}

function escapeIndex(index, count) {
	if (index < 0) {
		return (index + 0x1FFFFFFFFFFFFE) % count;
	}
	return index % count;
}

function calculateIndex(now, size) {
	var seconds = extractSeconds(now);
  	return escapeIndex(seconds, size);
}

function StorageManager(storage) {
	var self = this;
	self.getNumber = function(name, initializer) {
		var text = storage[name];
		if (text) {
			return parseInt(text);
		}
		var value = null;
		if (initializer) {
			value = initializer();
			self.setNumber(name, value)
		}
		return value;
	};
	self.setNumber = function(name, value) {
		storage[name] = String(value);
	};
	self.getArray = function(name, initializer) {
		var text = storage[name];
		if (text) {
			return JSON.parse(text);
		}
		var array = null;
		if (initializer) {
			array = initializer();
			self.setArray(name, array);
		}
		return array;
	};
	self.setArray = function(name, array) {
		storage[name] = JSON.stringify(array);
	};
}

function MovingSum(array) {
  	var sum = sumValues(array);
	this.update = function(offset, count, value) {
      	if (count > array.length) {
        	count = array.length;
        }
        for(var i = 1; i <= count; ++i) {
            var index = escapeIndex(offset + i, array.length);
            sum -= array[index];
            array[index] = 0;
        }
        var item = array[offset];
        sum += value;
		item += value;
        array[offset] = item;
      	return {
			sum: sum,
			item: item
		};
    };
}

function ActionProtection(storage, config) {
	var self = this;
	if (config == null) {
		config = {};
	}
	var actionName = config.actionName || 'action';
	var timeScope = config.timeScope || 10;
	var scopeLimit = config.scopeLimit || 5;
	var secondLimit = config.secondLimit || 2;
	var storageManager = new StorageManager(storage);
	var reportTime = storageManager.getNumber(actionName + '-time', function() {
		return 0;
	});
	var reportCounts = storageManager.getArray(actionName + '-counts', function() {
		return createArray(timeScope);
	});
	if (reportCounts.length != timeScope) {
		reportCounts = resizeArray(reportCounts, timeScope);
		storageManager.setArray(actionName + '-counts', reportCounts);
	}
  	var movingSum = new MovingSum(reportCounts);
	self.checkPermission = function() {
		var currentTime = Date.now();
		var idleDuration = extractSeconds(currentTime - reportTime); // since last action
		var currentIndex = calculateIndex(currentTime, timeScope);
      	var currentState = movingSum.update(currentIndex, idleDuration, 1);
        reportTime = currentTime;
        storageManager.setNumber(actionName + '-time', reportTime);
        storageManager.setArray(actionName + '-counts', reportCounts);
		if (currentState.sum > scopeLimit) {
			return false;
        }
		if (currentState.item > secondLimit) {
            return false;
        }
		return true;
	};
}

// Usage example:

// we keep information in localStorage
// we monitor last 10s
// we let for 5 login tries during last 10s
// we let for 2 login tries in one second

var config = {
	actionName: 'user-login', // lets use it as prefix in local storage
	timeScope: 10, // default monitored time scope in seconds
	scopeLimit: 5, // default max 5 tries in 10 seconds (in monitored time)
	secondLimit: 2 // default max 2 tries in 1 second
};
var protection = new ActionProtection(window.localStorage, config);

function checkPermission(time) {
	var action = function() {
        // permision checking with action reporting
    	console.log(protection.checkPermission());
    };
  	setTimeout(action, time);
}

// for first run button clicking or after 10 seconds result will be:
//     true true false true true false
//   first false is caused becasue of permitted 2 login tries per 1s
//   second false is caused becasue of permitted 5 login tries per 10s
checkPermission(   0); // checked in 0 second
checkPermission(   0); // checked in 0 second
checkPermission( 100); // checked in 0.1 second
checkPermission(2000); // checked in 2nd second
checkPermission(3000); // checked in 3rd second
checkPermission(5000); // checked in 5th second
Native Advertising
🚀
Get your tech brand or product in front of software developers.
For more information Contact us
Dirask - we help you to
solve coding problems.
Ask question.

❤️💻 🙂

Join