Accept cookie authentication only when CSRF header is provided (and correct)

Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
DL6ER 2023-06-04 18:39:49 +02:00
parent af4ce5cbba
commit 813509841b
No known key found for this signature in database
GPG Key ID: 00135ACBD90B28DD
7 changed files with 52 additions and 7 deletions

View File

@ -159,7 +159,7 @@ int api_handler(struct mg_connection *conn, void *ignored)
}
// Verify requesting client is allowed to see this resource
if(api_request[i].require_auth && check_client_auth(&api) == API_AUTH_UNAUTHORIZED)
if(api_request[i].require_auth && check_client_auth(&api, true) == API_AUTH_UNAUTHORIZED)
{
unauthorized = true;
break;

View File

@ -81,7 +81,7 @@ int api_list(struct ftl_conn *api);
int api_group(struct ftl_conn *api);
// Auth method
int check_client_auth(struct ftl_conn *api);
int check_client_auth(struct ftl_conn *api, const bool is_api);
int api_auth(struct ftl_conn *api);
void delete_all_sessions(void);
int api_auth_sessions(struct ftl_conn *api);

View File

@ -74,7 +74,7 @@ static struct {
// Can we validate this client?
// Returns -1 if not authenticated or expired
// Returns >= 0 for any valid authentication
int check_client_auth(struct ftl_conn *api)
int check_client_auth(struct ftl_conn *api, const bool is_api)
{
// Is the user requesting from localhost?
// This may be allowed without authentication depending on the configuration
@ -158,6 +158,28 @@ int check_client_auth(struct ftl_conn *api)
const time_t now = time(NULL);
log_debug(DEBUG_API, "Read sid=\"%s\" from %s", sid, sid_source);
// If the SID has been sent through a cookie, we require a CSRF token in
// the header to be sent along with the request for any API requests
char csrf[SID_SIZE];
const bool need_csrf = strcmp(sid_source, "cookie") == 0;
if(need_csrf && is_api)
{
const char *csrf_header = NULL;
// Try to extract CSRF token from header
if((csrf_header = mg_get_header(api->conn, "X-CSRF-TOKEN")) != NULL)
{
// Copy CSRF string
strncpy(csrf, csrf_header, SID_SIZE - 1u);
// Zero terminate CSRF string
csrf[SID_SIZE-1] = '\0';
}
else
{
log_debug(DEBUG_API, "API Authentication: FAIL (Cookie authentication without CSRF token)");
return API_AUTH_UNAUTHORIZED;
}
}
for(unsigned int i = 0; i < API_MAX_CLIENTS; i++)
{
if(auth_data[i].used &&
@ -165,6 +187,12 @@ int check_client_auth(struct ftl_conn *api)
strcmp(auth_data[i].remote_addr, api->request->remote_addr) == 0 &&
strcmp(auth_data[i].sid, sid) == 0)
{
if(need_csrf && strcmp(auth_data[i].csrf, csrf) != 0)
{
log_debug(DEBUG_API, "API Authentication: FAIL (CSRF token mismatch, recevied \"%s\", expected \"%s\")",
csrf, auth_data[i].csrf);
return API_AUTH_UNAUTHORIZED;
}
user_id = i;
break;
}
@ -203,7 +231,10 @@ int check_client_auth(struct ftl_conn *api)
}
}
else
{
log_debug(DEBUG_API, "API Authentication: FAIL (SID invalid/expired)");
return API_AUTH_UNAUTHORIZED;
}
api->user_id = user_id;
@ -260,6 +291,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
JSON_ADD_BOOL_TO_OBJECT(session, "valid", true);
JSON_ADD_BOOL_TO_OBJECT(session, "totp", strlen(config.webserver.api.totp_secret.v.s) > 0);
JSON_REF_STR_IN_OBJECT(session, "sid", auth_data[user_id].sid);
JSON_REF_STR_IN_OBJECT(session, "csrf", auth_data[user_id].csrf);
JSON_ADD_NUMBER_TO_OBJECT(session, "validity", auth_data[user_id].valid_until - now);
JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
JSON_ADD_BOOL_TO_OBJECT(json, "dns", dns);
@ -384,7 +416,7 @@ int api_auth(struct ftl_conn *api)
}
// Did the client authenticate before and we can validate this?
int user_id = check_client_auth(api);
int user_id = check_client_auth(api, false);
// If this is a valid session, we can exit early at this point
if(user_id != API_AUTH_UNAUTHORIZED)

View File

@ -214,6 +214,7 @@ components:
required:
- valid
- sid
- csrf
- validity
- totp
properties:
@ -227,6 +228,10 @@ components:
type: string
description: Session ID
nullable: true
csrf:
type: string
description: CSRF token
nullable: true
validity:
type: integer
description: Remaining lifetime of this session unless refreshed (seconds)
@ -354,6 +359,7 @@ components:
valid: true
totp: false
sid: null
csrf: null
validity: 300
dns: true
no_login_required:
@ -363,6 +369,7 @@ components:
valid: true
totp: false
sid: null
csrf: null
validity: -1
dns: true
login_required:
@ -372,6 +379,7 @@ components:
valid: false
totp: false
sid: null
csrf: null
validity: -1
dns: true
login_failed:
@ -381,6 +389,7 @@ components:
valid: false
totp: false
sid: null
csrf: null
validity: -1
dns: true
dns_failure:
@ -390,6 +399,7 @@ components:
valid: false
totp: false
sid: null
csrf: null
validity: -1
dns: false
errors:

View File

@ -114,11 +114,12 @@ int request_handler(struct mg_connection *conn, void *cbdata)
}
// Every LUA page except admin/login requires authentication
const int authorized = check_client_auth(&api, false) != API_AUTH_UNAUTHORIZED;
if(!login)
{
// This is not the login page - check if the user is authenticated
// Check if the user is authenticated
if(check_client_auth(&api) == API_AUTH_UNAUTHORIZED)
if(!authorized)
{
// Append query string to target
char *target = NULL;
@ -169,7 +170,7 @@ int request_handler(struct mg_connection *conn, void *cbdata)
{
// This is the login page - check if the user is already authenticated
// Check if the user is authenticated
if(check_client_auth(&api) != API_AUTH_UNAUTHORIZED)
if(authorized)
{
// User is already authenticated, redirect to index page
log_web("User is already authenticated, redirecting to %s", config.webserver.paths.webhome.v.s);

View File

@ -87,7 +87,9 @@ class FTLAPI():
elif authenticate == AuthenticationMethods.BODY:
json_data = {"sid": self.session['sid'] }
elif authenticate == AuthenticationMethods.COOKIE:
# Cookie authentication needs both the session ID and the CSRF header
cookies = {"sid": self.session['sid'] }
headers = { "X-CSRF-Token": self.session['csrf'] }
self.auth_method = authenticate.name

View File

@ -268,7 +268,7 @@ class ResponseVerifyer():
# Dive into the example to get to the property we want
for p in props:
if p not in example:
self.errors.append(f"Example {flat_path} does not have an '{p}' item")
self.errors.append(f"Example {t} does not have an '{p}' item")
return False
example = example[p]
# Check if the type of the example matches the type we defined in the API specs