Add rate-limiting on password login attempts
Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
parent
de7227347b
commit
2141db3d64
|
@ -509,7 +509,8 @@ int api_auth(struct ftl_conn *api)
|
|||
// else: Login attempt
|
||||
// - Client tries to authenticate using a password, or
|
||||
// - There no password on this machine
|
||||
if(empty_password ? true : verify_password(password, config.webserver.api.pwhash.v.s))
|
||||
const enum password_result result = empty_password ? true : verify_password(password, config.webserver.api.pwhash.v.s, true);
|
||||
if(result == PASSWORD_CORRECT)
|
||||
{
|
||||
// Accepted
|
||||
|
||||
|
@ -604,6 +605,15 @@ int api_auth(struct ftl_conn *api)
|
|||
log_warn("No free API seats available, not authenticating client");
|
||||
}
|
||||
}
|
||||
else if(result == PASSWORD_RATE_LIMITED)
|
||||
{
|
||||
// Rate limited
|
||||
log_debug(DEBUG_API, "API: Login attempt rate-limited");
|
||||
return send_json_error(api, 429,
|
||||
"too_many_requests",
|
||||
"Too many requests",
|
||||
NULL);
|
||||
}
|
||||
else
|
||||
{
|
||||
log_debug(DEBUG_API, "API: Password incorrect: '%s'", password);
|
||||
|
|
|
@ -287,7 +287,7 @@ static const char *getJSONvalue(struct conf_item *conf_item, cJSON *elem, struct
|
|||
char *pwhash = strlen(elem->valuestring) > 0 ? create_password(elem->valuestring) : strdup("");
|
||||
|
||||
// Verify that the password hash is valid
|
||||
const bool verfied = verify_password(elem->valuestring, pwhash);
|
||||
const bool verfied = verify_password(elem->valuestring, pwhash, false) == PASSWORD_CORRECT;
|
||||
|
||||
if(!verfied)
|
||||
{
|
||||
|
|
|
@ -79,6 +79,14 @@ components:
|
|||
allOf:
|
||||
- $ref: 'common.yaml#/components/errors/unauthorized'
|
||||
- $ref: 'common.yaml#/components/schemas/took'
|
||||
'429':
|
||||
description: Too Many Requests
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: 'common.yaml#/components/errors/too_many_requests'
|
||||
- $ref: 'common.yaml#/components/schemas/took'
|
||||
delete:
|
||||
summary: Delete session
|
||||
tags:
|
||||
|
|
|
@ -53,6 +53,26 @@ components:
|
|||
nullable: true
|
||||
description: "No additional data available"
|
||||
example: null
|
||||
too_many_requests:
|
||||
type: object
|
||||
description: "Too many requests (rate limiting)"
|
||||
properties:
|
||||
error:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
description: "Machine-readable error type"
|
||||
example: "too_many_requests"
|
||||
message:
|
||||
type: string
|
||||
description: "Human-readable error message"
|
||||
example: "Too many requests"
|
||||
hint:
|
||||
type: string
|
||||
nullable: true
|
||||
description: "No additional data available"
|
||||
example: null
|
||||
headers:
|
||||
Location:
|
||||
description: Location of created resource
|
||||
|
|
|
@ -162,7 +162,7 @@ static bool readStringValue(struct conf_item *conf_item, const char *value, stru
|
|||
char *pwhash = strlen(value) > 0 ? create_password(value) : strdup("");
|
||||
|
||||
// Verify that the password hash is valid
|
||||
const bool verfied = verify_password(value, pwhash);
|
||||
const bool verfied = verify_password(value, pwhash, false) == PASSWORD_CORRECT;
|
||||
|
||||
if(!verfied)
|
||||
{
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
#include "password.h"
|
||||
// genrandom() with fallback
|
||||
#include "daemon.h"
|
||||
// sleepms()
|
||||
#include "timers.h"
|
||||
|
||||
// Randomness generator
|
||||
#include "webserver/x509.h"
|
||||
|
@ -305,16 +307,39 @@ char * __attribute__((malloc)) create_password(const char *password)
|
|||
return balloon_password(password, salt, true);
|
||||
}
|
||||
|
||||
bool verify_password(const char *password, const char* pwhash)
|
||||
char verify_password(const char *password, const char* pwhash, const bool rate_limiting)
|
||||
{
|
||||
// No password supplied
|
||||
if(password == NULL || password[0] == '\0')
|
||||
return false;
|
||||
return PASSWORD_INCORRECT;
|
||||
|
||||
// No password set
|
||||
if(pwhash == NULL || pwhash[0] == '\0')
|
||||
return true;
|
||||
return PASSWORD_CORRECT;
|
||||
|
||||
// Check if there has already been one login attempt within this second
|
||||
static time_t last_password_attempt = 0;
|
||||
static unsigned int num_password_attempts = 0;
|
||||
if(rate_limiting &&
|
||||
last_password_attempt > 0 &&
|
||||
last_password_attempt == time(NULL))
|
||||
{
|
||||
// Check if we have reached the maximum number of attempts
|
||||
if(++num_password_attempts > MAX_PASSWORD_ATTEMPTS_PER_SECOND)
|
||||
{
|
||||
// Rate limit reached
|
||||
sleepms(250);
|
||||
return PASSWORD_RATE_LIMITED;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reset counter
|
||||
num_password_attempts = 1;
|
||||
last_password_attempt = time(NULL);
|
||||
}
|
||||
|
||||
// Check password hash format
|
||||
if(pwhash[0] == '$')
|
||||
{
|
||||
// Parse PHC string
|
||||
|
@ -323,9 +348,9 @@ bool verify_password(const char *password, const char* pwhash)
|
|||
uint8_t *salt = NULL;
|
||||
uint8_t *config_hash = NULL;
|
||||
if(!parse_PHC_string(pwhash, &s_cost, &t_cost, &salt, &config_hash))
|
||||
return false;
|
||||
return PASSWORD_INCORRECT;
|
||||
if(salt == NULL || config_hash == NULL)
|
||||
return false;
|
||||
return PASSWORD_INCORRECT;
|
||||
char *supplied = balloon_password(password, salt, false);
|
||||
const bool result = memcmp(config_hash, supplied, SHA256_DIGEST_SIZE) == 0;
|
||||
|
||||
|
@ -336,7 +361,7 @@ bool verify_password(const char *password, const char* pwhash)
|
|||
if(config_hash != NULL)
|
||||
free(config_hash);
|
||||
|
||||
return result;
|
||||
return result ? PASSWORD_CORRECT : PASSWORD_INCORRECT;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -361,7 +386,7 @@ bool verify_password(const char *password, const char* pwhash)
|
|||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result ? PASSWORD_CORRECT : PASSWORD_INCORRECT;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,16 @@
|
|||
|
||||
void sha256_raw_to_hex(uint8_t *data, char *buffer);
|
||||
char *create_password(const char *password) __attribute__((malloc));
|
||||
bool verify_password(const char *password, const char *pwhash);
|
||||
char verify_password(const char *password, const char *pwhash, const bool rate_limiting);
|
||||
int run_performance_test(void);
|
||||
|
||||
enum password_result {
|
||||
PASSWORD_INCORRECT = 0,
|
||||
PASSWORD_CORRECT = 1,
|
||||
PASSWORD_RATE_LIMITED = -1
|
||||
} __attribute__((packed));
|
||||
|
||||
// The maximum number of password attempts per second
|
||||
#define MAX_PASSWORD_ATTEMPTS_PER_SECOND 3
|
||||
|
||||
#endif //PASSWORD_H
|
||||
|
|
|
@ -43,9 +43,10 @@ class FTLAPI():
|
|||
self.verbose = False
|
||||
|
||||
# Login to FTL API
|
||||
self.login(password)
|
||||
if self.session is None or 'valid' not in self.session or not self.session['valid']:
|
||||
raise Exception("Could not login to FTL API")
|
||||
if password is not None:
|
||||
self.login(password)
|
||||
if self.session is None or 'valid' not in self.session or not self.session['valid']:
|
||||
raise Exception("Could not login to FTL API")
|
||||
|
||||
def login(self, password: str = None):
|
||||
# Check if we even need to login
|
||||
|
@ -65,6 +66,8 @@ class FTLAPI():
|
|||
return
|
||||
|
||||
response = self.POST("/api/auth", {"password": password})
|
||||
if "error" in response:
|
||||
raise Exception("FTL returned error: " + json.dumps(response["error"]))
|
||||
if 'session' not in response:
|
||||
raise Exception("FTL returned invalid response item")
|
||||
self.session = response["session"]
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# Script that sends a number of randomly generated passwords to the
|
||||
# /api/auth endpoint checking that rate limiting is enforced
|
||||
import random
|
||||
import string
|
||||
from libs.FTLAPI import FTLAPI
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create FTLAPI object
|
||||
ftl = FTLAPI("http://127.0.0.1:8080")
|
||||
|
||||
# Try to login with random passwords
|
||||
for i in range(0, 100):
|
||||
pw = "".join(random.choices(string.printable, k=random.randint(1, 64)))
|
||||
try:
|
||||
ftl.login(pw)
|
||||
except Exception as e:
|
||||
if "too_many_requests" in str(e):
|
||||
print("Rate-limited on attempt no. " + str(i))
|
||||
exit(0)
|
||||
else:
|
||||
print("Unexpected error: " + str(e))
|
||||
exit(1)
|
||||
print("Rate-limiting was not enforced")
|
||||
exit(1)
|
Loading…
Reference in New Issue