summaryrefslogtreecommitdiff
path: root/src/mod_csrf.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/mod_csrf.c')
-rw-r--r--src/mod_csrf.c537
1 files changed, 537 insertions, 0 deletions
diff --git a/src/mod_csrf.c b/src/mod_csrf.c
new file mode 100644
index 00000000..37d70f7a
--- /dev/null
+++ b/src/mod_csrf.c
@@ -0,0 +1,537 @@
+#include "base.h"
+#include "log.h"
+#include "buffer.h"
+#include "base64.h"
+
+#include "plugin.h"
+
+#include <ctype.h>
+#include <stdlib.h>
+#include <string.h>
+
+#if defined (USE_OPENSSL)
+
+#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;
+}
+
+/* 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;
+ HMAC_CTX_init(&ctx);
+
+ 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;
+
+ HMAC_CTX_cleanup(&ctx);
+ return 1;
+
+err:
+ HMAC_CTX_cleanup(&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;
+}
+
+#else
+
+/* if we don't have openssl support, this plugin does nothing */
+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");
+ return 0;
+}
+
+#endif