Browse Source

[mod_csrf] module to aid against csrf attacks

personal/stbuehler/mod-csrf
Stefan Bühler 6 years ago
parent
commit
73ca208c81
  1. 6
      configure.ac
  2. 1
      doc/config/conf.d/Makefile.am
  3. 101
      doc/config/conf.d/csrf.conf
  4. 5
      src/CMakeLists.txt
  5. 11
      src/Makefile.am
  6. 1
      src/SConscript
  7. 542
      src/mod_csrf.c

6
configure.ac

@ -1162,6 +1162,12 @@ else
disable_feature="$disable_feature $features"
fi
plugin="mod_csrf"
if test "x$SSL_LIB" \!= x; then
do_build="$do_build $plugin"
else
no_build="$no_build $plugin"
fi
dnl output

1
doc/config/conf.d/Makefile.am

@ -3,6 +3,7 @@ EXTRA_DIST=access_log.conf \
cgi.conf \
cml.conf \
compress.conf \
csrf.conf \
debug.conf \
dirlisting.conf \
evhost.conf \

101
doc/config/conf.d/csrf.conf

@ -0,0 +1,101 @@
#######################################################################
##
## CSRF Protection Module
## ----------------
##
## Make sure to load "mod_auth" before (or whatever is supposed to set
## REMOTE_USER).
##
server.modules += ( "mod_csrf" )
## Activate CSRF module:
##
## If module is activated and (REMOTE_USER not empty or
## csrf.require-user is disabled) the module makes sure the client
## receives a token unless it has a valid token which is less than
## csrf.ttl/4 seconds old
##
## Default:
# csrf.activate = "disable"
## Use conditions to activate protection for certain URLs:
# $HTTP["url"] =~ "^/(someurl|cgi-bin)/(.+)" {
# csrf.activate = "enable"
# }
## CSRF-protect all requests
##
## As soon as CSRF is activated all requests are by default protected
## (event GET requests), i.e. require the client to send a valid CSRF
## token. You can disable protection to just make sure the client
## receives a valid token for future requests.
##
## Default:
# csrf.protect = "enable"
## Don't require CSRF for GET requests (but still send tokens in
## response)
# csrf.activate = "enable"
# $HTTP["request-method"] == "GET" {
# csrf.protect = "disable"
# }
## Require a logged in user
##
## To prevent mistakes in the config by default a REMOTE_USER is
## required. If your users are authenticated in another way (say client
## ip address) and you don't have REMOTE_USER you still can use this
## module to prevent CSRF from external sites, but you need to disable
## this option.
##
## Default:
# csrf.require-user = "enable"
## Activate debug logging
##
## Default:
# csrf.debug = "disable"
## Hash function to use for HMAC
##
## Supports whatever your openssl library recognizes
##
## Default:
# csrf.hash = "sha256"
## HTTP Header name for CSRF tokens
##
## Header name for both HTTP requests and HTTP responses.
##
## A client application needs to read this header from responses and
## copy it into new requests to gain access to protected resources.
##
## Default:
# csrf.header = "X-Csrf-Token"
## Secret key for HMAC to "sign" token data with
##
## Only set this if you need tokens to stay valid across a load-balanced
## setup. If set needs to be at least 20 characters long. Use some
## secure "password" generator if you need this (e.g. "pwgen -s 32 1")
##
## Default: create a random 20-byte secret on each restart
# csrf.secret = "..."
## Default Time-To-Live for a token
##
## How long (in seconds) a token is valid; after csrf.ttl/4 seconds the
## module will send the client a new token.
##
## Your client applications still need to be able to handle token
## timeouts (i.e. retry requests with the new token they received).
##
## A token will also be valid csrf.ttl seconds *before* its timestamp
## (to avoid problems with time sync between multiple nodes in a
## cluster)
##
## Default: (10 minutes)
# csrf.ttl = 600
##
#######################################################################

5
src/CMakeLists.txt

@ -756,6 +756,11 @@ if(WITH_LDAP)
target_link_libraries(mod_vhostdb_ldap ${L_MOD_AUTHN_LDAP})
endif()
if(HAVE_LIBCRYPT)
add_and_install_library(mod_csrf mod_csrf.c)
target_link_libraries(mod_csrf crypt)
endif()
if(HAVE_ZLIB_H)
if(HAVE_BZLIB_H)
target_link_libraries(mod_compress ${ZLIB_LIBRARY} bz2)

11
src/Makefile.am

@ -145,6 +145,13 @@ mod_cml_la_LDFLAGS = $(common_module_ldflags)
mod_cml_la_LIBADD = $(MEMCACHED_LIB) $(common_libadd) $(LUA_LIBS) -lm
endif
if BUILD_WITH_OPENSSL
lib_LTLIBRARIES += mod_csrf.la
mod_csrf_la_SOURCES = mod_csrf.c
mod_csrf_la_LDFLAGS = $(common_module_ldflags)
mod_csrf_la_LIBADD = $(CRYPT_LIB) $(common_libadd)
endif
if BUILD_MOD_TRIGGER_B4_DL
lib_LTLIBRARIES += mod_trigger_b4_dl.la
mod_trigger_b4_dl_la_SOURCES = mod_trigger_b4_dl.c
@ -455,6 +462,10 @@ lighttpd_SOURCES += mod_cml.c mod_cml_lua.c mod_cml_funcs.c \
lighttpd_CPPFLAGS += $(LUA_CFLAGS)
lighttpd_LDADD += $(LUA_LIBS) -lm
endif
if BUILD_WITH_OPENSSL
lighttpd_SOURCES += mod_csrf.c
lighttpd_LDADD += $(CRYPTO_LIB)
endif
if BUILD_WITH_KRB5
lighttpd_SOURCES += mod_authn_gssapi.c
lighttpd_LDADD += $(KRB5_LIB)

1
src/SConscript

@ -95,6 +95,7 @@ modules = {
'mod_access' : { 'src' : [ 'mod_access.c' ] },
'mod_alias' : { 'src' : [ 'mod_alias.c' ] },
'mod_cgi' : { 'src' : [ 'mod_cgi.c' ] },
'mod_csrf' : { 'src' : [ 'mod_csrf.c' ], 'lib' : [ env['LIBCRYPT'] ] },
'mod_fastcgi' : { 'src' : [ 'mod_fastcgi.c' ] },
'mod_scgi' : { 'src' : [ 'mod_scgi.c' ] },
'mod_extforward' : { 'src' : [ 'mod_extforward.c' ] },

542
src/mod_csrf.c

@ -0,0 +1,542 @@
#include "base.h"
#include "log.h"
#include "buffer.h"
#include "base64.h"
#include "plugin.h"
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/rand.h>
#include <openssl/hmac.h>
/* Token:
*
* the token protects a message + additional data:
*
* "message":
* - 1 byte: version (0x01)
* - 8 bytes: uint64_t big endian timestamp in seconds since epoch
* "additional data":
* - REMOTE_USER as simple string (without null termination); can be empty
* REMOTE_USER is usually set by mod_auth; you must load mod_auth before mod_csrf!
*
* The token consists of the base64-encoding of "message" and a checksum (HMAC)
*/
#define SECRET_SIZE_BYTES 20 /* secret to automatically generate as fallback */
#define TOKEN_DEFAULT_TTL (10*60) /* in seconds: 10 minutes */
#define TOKEN_DEFAULT_HEADER "X-Csrf-Token"
typedef enum {
TOKEN_CHECK_OK,
TOKEN_CHECK_OK_RENEW,
TOKEN_CHECK_FAILED
} token_check_result;
/* plugin config for all request/connections */
typedef struct {
unsigned short activate:1;
unsigned short protect:1;
unsigned short require_user:1;
unsigned short debug:1;
int ttl;
const EVP_MD* hash;
buffer* header;
buffer* secret;
} plugin_config;
typedef struct {
PLUGIN_DATA;
plugin_config* config_storage;
plugin_config conf;
} plugin_data;
static buffer* create_message(void) {
uint64_t now = (uint64_t) time(NULL);
buffer* msg = buffer_init();
{
char* raw = buffer_string_prepare_append(msg, 1 + sizeof(now));
size_t i;
*raw++ = 1; /* CSRF token "version" */
/* store "now" as big endian */
for (i = sizeof(now); i-- > 0; ) {
raw[i] = (char) (now);
now >>= 8;
}
buffer_commit(msg, 1 + sizeof(now));
}
return msg;
}
/* returns 0 on failure */
static int parse_and_check_message(server *srv, plugin_data* p, char const* msg, size_t msg_len, uint64_t* ts) {
if (0 == msg_len) {
if (p->conf.debug) {
log_error_write(srv, __FILE__, __LINE__, "s",
"invalid token length: empty message");
}
return 0;
}
if (msg[0] != 1) {
if (p->conf.debug) {
log_error_write(srv, __FILE__, __LINE__, "sx",
"invalid token version, expected 0x01, got: ",
(int)(unsigned char)msg[0]);
}
return 0; /* CSRF token "version" check */
}
if (msg_len != 9) {
if (p->conf.debug) {
log_error_write(srv, __FILE__, __LINE__, "sd",
"invalid token message length, expected 9, got: ",
(int) msg_len);
}
return 0;
}
/* parse timestamp */
{
uint64_t tsn = 0;
size_t i;
for (i = 1; i < 9; ++i) {
tsn = (tsn << 8) | (unsigned char)(msg[i]);
}
*ts = tsn;
}
return 1;
}
static HMAC_CTX* csrf_hmac_ctx_new(void) {
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
return HMAC_CTX_new();
#else
HMAC_CTX* ctx = malloc(sizeof(HMAC_CTX));
HMAC_CTX_init(&ctx);
return ctx;
#endif
}
static void csrf_hmac_ctx_free(HMAC_CTX* ctx) {
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
HMAC_CTX_free(ctx);
#else
HMAC_CTX_cleanup(ctx);
free(ctx);
#endif
}
/* returns 0 on failure */
static int hmac_message(plugin_data* p, char* digest, char const* msg, size_t msg_len, const char* user) {
HMAC_CTX* ctx = csrf_hmac_ctx_new();
force_assert(buffer_string_length(p->conf.secret) >= SECRET_SIZE_BYTES);
force_assert(NULL != p->conf.hash);
if (!HMAC_Init_ex(ctx, CONST_BUF_LEN(p->conf.secret), p->conf.hash, NULL)) goto err;
if (!HMAC_Update(ctx, (unsigned char const*) msg, msg_len)) goto err;
/* message must be "self-terminating" and length-checked!
* -> just append user to message if there is one
*/
if (user && !HMAC_Update(ctx, (unsigned char const*) user, strlen(user))) goto err;
if (!HMAC_Final(ctx, (unsigned char*) digest, NULL)) goto err;
csrf_hmac_ctx_free(ctx);
return 1;
err:
csrf_hmac_ctx_free(ctx);
return 0;
}
/* returns 0 on failure */
static int append_token(server* srv, plugin_data* p, buffer* buf, const char* user) {
buffer* msg = create_message();
{
const size_t digest_len = EVP_MD_size(p->conf.hash);
char* digest = buffer_string_prepare_append(msg, digest_len);
if (!hmac_message(p, digest, CONST_BUF_LEN(msg), user)) {
buffer_free(msg);
log_error_write(srv, __FILE__, __LINE__, "s",
"failed to create token digest");
return 0;
}
buffer_commit(msg, digest_len);
}
buffer_append_base64_encode_no_padding(buf, (unsigned char const*) CONST_BUF_LEN(msg), BASE64_STANDARD);
buffer_free(msg);
return 1;
}
static token_check_result verify_token(server* srv, plugin_data* p, char const* token, size_t token_len, const char* user) {
uint64_t ts;
{
const size_t digest_len = EVP_MD_size(p->conf.hash);
size_t msg_len;
buffer* decoded_token = buffer_init();
if (NULL == buffer_append_base64_decode(decoded_token, token, token_len, BASE64_STANDARD)) {
buffer_free(decoded_token);
if (p->conf.debug) {
log_error_write(srv, __FILE__, __LINE__, "s",
"failed to decode base64 token");
}
return TOKEN_CHECK_FAILED;
}
if (buffer_string_length(decoded_token) < digest_len) {
if (p->conf.debug) {
log_error_write(srv, __FILE__, __LINE__, "s",
"token too short for digest");
}
return TOKEN_CHECK_FAILED;
}
msg_len = buffer_string_length(decoded_token) - digest_len;
if (!parse_and_check_message(srv, p, decoded_token->ptr, msg_len, &ts)) {
buffer_free(decoded_token);
return TOKEN_CHECK_FAILED;
}
{
buffer* digest_buf = buffer_init();
char* digest = buffer_string_prepare_append(digest_buf, digest_len);
if (!hmac_message(p, digest, decoded_token->ptr, msg_len, user)
|| 0 != strncmp(decoded_token->ptr + msg_len, (char const*) digest, digest_len))
{
buffer_free(decoded_token);
buffer_free(digest_buf);
if (p->conf.debug) {
log_error_write(srv, __FILE__, __LINE__, "s",
"token digest didn't match");
}
return TOKEN_CHECK_FAILED;
}
buffer_free(digest_buf);
}
buffer_free(decoded_token);
}
{
int64_t timediff = (int64_t)(ts - (uint64_t)time(NULL));
/* accept "ttl" seconds in BOTH directions - usually you shouldn't sign
* too much ahead of time (in case of multiple servers)
*/
if (timediff < -p->conf.ttl || timediff > p->conf.ttl) {
if (p->conf.debug) {
log_error_write(srv, __FILE__, __LINE__, "s",
"token expired");
}
return TOKEN_CHECK_FAILED; /* timeout */
}
if (timediff > p->conf.ttl / 4) return TOKEN_CHECK_OK_RENEW;
}
return TOKEN_CHECK_OK;
}
/* init the plugin data */
INIT_FUNC(mod_csrf_init) {
plugin_data* p;
p = calloc(1, sizeof(*p));
return p;
}
/* detroy the plugin data */
FREE_FUNC(mod_csrf_free) {
plugin_data* p = p_d;
UNUSED(srv);
if (!p) return HANDLER_GO_ON;
if (p->config_storage) {
size_t i;
for (i = 0; i < srv->config_context->used; i++) {
plugin_config* s = &p->config_storage[i];
buffer_free(s->header);
buffer_free(s->secret);
}
free(p->config_storage);
}
free(p);
return HANDLER_GO_ON;
}
/* handle plugin config and check values */
#define CSRF_CONFIG_ACTIVATE "csrf.activate"
#define CSRF_CONFIG_PROTECT "csrf.protect"
#define CSRF_CONFIG_REQUIRE_USER "csrf.require-user"
#define CSRF_CONFIG_DEBUG "csrf.debug"
#define CSRF_CONFIG_HASH "csrf.hash"
#define CSRF_CONFIG_HEADER "csrf.header"
#define CSRF_CONFIG_SECRET "csrf.secret"
#define CSRF_CONFIG_TTL "csrf.ttl"
SETDEFAULTS_FUNC(mod_csrf_set_defaults) {
plugin_data* p = p_d;
size_t i = 0;
buffer* hash = buffer_init();
config_values_t cv[] = {
{ CSRF_CONFIG_ACTIVATE, NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 0 */
{ CSRF_CONFIG_PROTECT, NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 1 */
{ CSRF_CONFIG_REQUIRE_USER, NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 2 */
{ CSRF_CONFIG_DEBUG, NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 3 */
{ CSRF_CONFIG_TTL, NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION }, /* 4 */
{ CSRF_CONFIG_HASH, NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 5 */
{ CSRF_CONFIG_HEADER, NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 6 */
{ CSRF_CONFIG_SECRET, NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 7 */
{ NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
};
p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config));
for (i = 0; i < srv->config_context->used; i++) {
data_config const* config = (data_config const*)srv->config_context->data[i];
plugin_config* s = &p->config_storage[i];
unsigned short activate = 0;
unsigned short protect = 1; /* protect by default (when activated) */
/* empty user not allowed by default to prevent mistakes in mod_auth/mod_csrf ordering */
unsigned short require_user = 1;
unsigned short debug = 0;
unsigned short ttl = TOKEN_DEFAULT_TTL;
s->hash = NULL;
s->header = buffer_init();
s->secret = buffer_init();
buffer_reset(hash);
cv[0].destination = &(activate);
cv[1].destination = &(protect);
cv[2].destination = &(require_user);
cv[3].destination = &(debug);
cv[4].destination = &(ttl);
cv[5].destination = hash;
cv[6].destination = s->header;
cv[7].destination = s->secret;
if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
return HANDLER_ERROR;
}
s->activate = activate;
s->protect = protect;
s->require_user = require_user;
s->debug = debug;
s->ttl = ttl;
if (!buffer_is_empty(s->secret) && buffer_string_length(s->secret) < SECRET_SIZE_BYTES) {
log_error_write(srv, __FILE__, __LINE__, "s",
CSRF_CONFIG_SECRET " too short");
return HANDLER_ERROR;
}
if (!buffer_is_empty(hash)) {
s->hash = EVP_get_digestbyname(hash->ptr);
if (NULL == s->hash) {
log_error_write(srv, __FILE__, __LINE__, "sb",
"couldn't find " CSRF_CONFIG_HASH ":",
hash);
return HANDLER_ERROR;
}
}
}
{
plugin_config* s = &p->config_storage[0];
if (NULL == s->hash) s->hash = EVP_sha256();
if (buffer_string_is_empty(s->header)) buffer_copy_string_len(s->header, CONST_STR_LEN(TOKEN_DEFAULT_HEADER));
if (buffer_string_is_empty(s->secret)) {
buffer_string_set_length(s->secret, SECRET_SIZE_BYTES);
if (RAND_bytes((unsigned char*) s->secret->ptr, SECRET_SIZE_BYTES) == 0) {
log_error_write(srv, __FILE__, __LINE__, "s",
"failed to generate secret key");
return HANDLER_ERROR;
}
}
}
return HANDLER_GO_ON;
}
#define PATCH(x) \
p->conf.x = s->x;
static int mod_csrf_patch_connection(server* srv, connection* con, plugin_data* p) {
size_t i, j;
plugin_config* s = &p->config_storage[0];
PATCH(activate);
PATCH(protect);
PATCH(require_user);
PATCH(debug);
PATCH(hash);
PATCH(header);
PATCH(secret);
PATCH(ttl);
/* skip the first, the global context */
for (i = 1; i < srv->config_context->used; i++) {
data_config* dc = (data_config*) srv->config_context->data[i];
s = &p->config_storage[i];
/* condition didn't match */
if (!config_check_cond(srv, con, dc)) continue;
/* merge config */
for (j = 0; j < dc->value->used; j++) {
data_unset* du = dc->value->data[j];
if (buffer_is_equal_string(du->key, CONST_STR_LEN(CSRF_CONFIG_ACTIVATE))) {
PATCH(activate);
} else if (buffer_is_equal_string(du->key, CONST_STR_LEN(CSRF_CONFIG_PROTECT))) {
PATCH(protect);
} else if (buffer_is_equal_string(du->key, CONST_STR_LEN(CSRF_CONFIG_REQUIRE_USER))) {
PATCH(require_user);
} else if (buffer_is_equal_string(du->key, CONST_STR_LEN(CSRF_CONFIG_DEBUG))) {
PATCH(debug);
} else if (buffer_is_equal_string(du->key, CONST_STR_LEN(CSRF_CONFIG_HASH))) {
PATCH(hash);
} else if (buffer_is_equal_string(du->key, CONST_STR_LEN(CSRF_CONFIG_HEADER))) {
PATCH(header);
} else if (buffer_is_equal_string(du->key, CONST_STR_LEN(CSRF_CONFIG_SECRET))) {
PATCH(secret);
} else if (buffer_is_equal_string(du->key, CONST_STR_LEN(CSRF_CONFIG_TTL))) {
PATCH(ttl);
}
}
}
return 0;
}
#undef PATCH
URIHANDLER_FUNC(mod_csrf_uri_handler) {
plugin_data* p = p_d;
buffer* csrf_req_header = NULL;
const char* user = NULL;
token_check_result result = TOKEN_CHECK_FAILED;
mod_csrf_patch_connection(srv, con, p);
if (!p->conf.activate) return HANDLER_GO_ON;
{
data_string* ds_user = (data_string*) array_get_element(con->environment, "REMOTE_USER");
if (NULL != ds_user) user = ds_user->value->ptr;
}
if (p->conf.require_user && (NULL == user || 0 == strlen(user))) {
if (p->conf.protect) {
con->http_status = 403;
con->mode = DIRECT;
if (p->conf.debug || con->conf.log_request_handling) {
log_error_write(srv, __FILE__, __LINE__, "s",
"require user to protect with csrf: user is missing -> rejecting request");
}
return HANDLER_FINISHED;
} else {
if (p->conf.debug) {
log_error_write(srv, __FILE__, __LINE__, "s",
"require user to activate csrf: user is missing -> not generating token");
}
/* only activate when we actually have a user */
return HANDLER_GO_ON;
}
}
{
data_string* ds_req_header = (data_string*) array_get_element(con->request.headers, p->conf.header->ptr);
if (NULL != ds_req_header) csrf_req_header = ds_req_header->value;
}
if (csrf_req_header) {
result = verify_token(srv, p, CONST_BUF_LEN(csrf_req_header), user);
}
switch (result) {
case TOKEN_CHECK_OK:
break;
default:
{
data_string* ds_resp_header = data_response_init();
if (p->conf.debug) {
log_error_write(srv, __FILE__, __LINE__, "s",
"old/invalid csrf token: sending new token");
}
buffer_copy_buffer(ds_resp_header->key, p->conf.header);
if (!append_token(srv, p, ds_resp_header->value, user)) {
ds_resp_header->free((data_unset*) ds_resp_header);
} else {
array_insert_unique(con->response.headers, (data_unset*) ds_resp_header);
}
}
break;
}
switch (result) {
case TOKEN_CHECK_OK:
case TOKEN_CHECK_OK_RENEW:
if (p->conf.debug || con->conf.log_request_handling) {
log_error_write(srv, __FILE__, __LINE__, "s",
"valid csrf token: accepting request");
}
break;
default:
if (p->conf.protect) {
con->http_status = 403;
con->mode = DIRECT;
if (p->conf.debug || con->conf.log_request_handling) {
log_error_write(srv, __FILE__, __LINE__, "s",
"missing/invalid csrf token: rejecting request");
}
return HANDLER_FINISHED;
}
break;
}
return HANDLER_GO_ON;
}
int mod_csrf_plugin_init(plugin* p);
int mod_csrf_plugin_init(plugin* p) {
p->version = LIGHTTPD_VERSION_ID;
p->name = buffer_init_string("csrf");
p->init = mod_csrf_init;
p->handle_uri_clean = mod_csrf_uri_handler;
p->set_defaults = mod_csrf_set_defaults;
p->cleanup = mod_csrf_free;
p->data = NULL;
return 0;
}
Loading…
Cancel
Save