Browse Source

Merge pull request #5490 from rspamd/vstakhov-ucl-fix

One of the major issues is that we should use safe parser flags for all inputs aside of the real configuration which we should really trust. It means that we need to disable macros, file variables and other UCL features when it is used to parse any potentially unsafe content. I do not treat it as a security vulnerability so far, as Rspamd operates with merely trusted content by HTTP by default, however, it is good to fix to avoid any potential future mususes or even exploits possibilities.
pull/5496/head
Vsevolod Stakhov 6 months ago
committed by GitHub
parent
commit
f60cd32049
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      contrib/libucl/lua_ucl.c
  2. 20
      contrib/libucl/ucl.h
  3. 10
      contrib/libucl/ucl_parser.c
  4. 32
      contrib/libucl/ucl_util.c
  5. 4
      src/client/rspamdclient.c
  6. 4
      src/controller.c
  7. 10
      src/fuzzy_storage.c
  8. 6
      src/libmime/lang_detection.c
  9. 2
      src/libserver/cfg_rcl.cxx
  10. 2
      src/libserver/cfg_utils.cxx
  11. 4
      src/libserver/dynamic_cfg.c
  12. 8
      src/libserver/roll_history.c
  13. 4
      src/libserver/rspamd_control.c
  14. 2
      src/libserver/symcache/symcache_impl.cxx
  15. 2
      src/libserver/worker_util.c
  16. 2
      src/lua/lua_cryptobox.c
  17. 8
      src/rspamadm/control.c
  18. 4
      src/rspamadm/signtool.c
  19. 2
      src/rspamd_proxy.c

12
contrib/libucl/lua_ucl.c

@ -1,5 +1,5 @@
/*
* Copyright 2024 Vsevolod Stakhov
* Copyright 2025 Vsevolod Stakhov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -694,7 +694,13 @@ static int
lua_ucl_parser_init(lua_State *L)
{
struct ucl_parser *parser, **pparser;
int flags = UCL_PARSER_NO_FILEVARS;
/*
* We disable file variables and macros by default, as
* the most use cases are parsing of JSON and not of the real
* files. Macros in the parser are very dangerous and should be used
* for trusted data only.
*/
int flags = UCL_PARSER_SAFE_FLAGS;
if (lua_gettop(L) >= 1) {
flags = lua_tonumber(L, 1);
@ -1091,7 +1097,7 @@ lua_ucl_parser_validate(lua_State *L)
}
}
else if (lua_type(L, 2) == LUA_TSTRING) {
schema_parser = ucl_parser_new(0);
schema_parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
schema_file = luaL_checkstring(L, 2);
if (!ucl_parser_add_file(schema_parser, schema_file)) {

20
contrib/libucl/ucl.h

@ -1,3 +1,19 @@
/*
* Copyright 2025 Vsevolod Stakhov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Copyright (c) 2013-2015, Vsevolod Stakhov
* All rights reserved.
*
@ -159,6 +175,10 @@ typedef enum ucl_parser_flags {
UCL_PARSER_NO_FILEVARS = (1 << 6) /** Do not set file vars */
} ucl_parser_flags_t;
#define UCL_PARSER_SAFE_FLAGS (UCL_PARSER_NO_TIME | \
UCL_PARSER_NO_IMPLICIT_ARRAYS | \
UCL_PARSER_DISABLE_MACRO | \
UCL_PARSER_NO_FILEVARS)
/**
* String conversion flags, that are used in #ucl_object_fromstring_common function.
*/

10
contrib/libucl/ucl_parser.c

@ -1246,10 +1246,12 @@ ucl_parser_process_object_element (struct ucl_parser *parser, ucl_object_t *nobj
container = parser->stack->obj->value.ov;
DL_FOREACH (parser->stack->obj, cur) {
tobj = __DECONST (ucl_object_t *, ucl_hash_search_obj (cur->value.ov, nobj));
if (cur->type == UCL_OBJECT) {
tobj = __DECONST (ucl_object_t *, ucl_hash_search_obj (cur->value.ov, nobj));
if (tobj != NULL) {
break;
if (tobj != NULL) {
break;
}
}
}
@ -3165,7 +3167,7 @@ ucl_parser_add_string (struct ucl_parser *parser, const char *data,
bool
ucl_set_include_path (struct ucl_parser *parser, ucl_object_t *paths)
{
if (parser == NULL || paths == NULL) {
if (parser == NULL || paths == NULL || paths->type != UCL_ARRAY) {
return false;
}

32
contrib/libucl/ucl_util.c

@ -3148,6 +3148,10 @@ ucl_object_frombool (bool bv)
bool
ucl_array_append (ucl_object_t *top, ucl_object_t *elt)
{
if (top->type != UCL_ARRAY) {
return false;
}
UCL_ARRAY_GET (vec, top);
if (elt == NULL || top == NULL) {
@ -3177,6 +3181,10 @@ e0:
bool
ucl_array_prepend (ucl_object_t *top, ucl_object_t *elt)
{
if (top->type != UCL_ARRAY) {
return false;
}
UCL_ARRAY_GET (vec, top);
if (elt == NULL || top == NULL) {
@ -3242,6 +3250,10 @@ e0:
ucl_object_t *
ucl_array_delete (ucl_object_t *top, ucl_object_t *elt)
{
if (top->type != UCL_ARRAY) {
return NULL;
}
UCL_ARRAY_GET (vec, top);
ucl_object_t *ret = NULL;
unsigned i;
@ -3290,6 +3302,10 @@ ucl_array_tail (const ucl_object_t *top)
ucl_object_t *
ucl_array_pop_last (ucl_object_t *top)
{
if (top->type != UCL_ARRAY) {
return NULL;
}
UCL_ARRAY_GET (vec, top);
ucl_object_t **obj, *ret = NULL;
@ -3306,6 +3322,10 @@ ucl_array_pop_last (ucl_object_t *top)
ucl_object_t *
ucl_array_pop_first (ucl_object_t *top)
{
if (top->type != UCL_ARRAY) {
return NULL;
}
UCL_ARRAY_GET (vec, top);
ucl_object_t **obj, *ret = NULL;
@ -3338,6 +3358,10 @@ ucl_array_size (const ucl_object_t *top)
const ucl_object_t *
ucl_array_find_index (const ucl_object_t *top, unsigned int index)
{
if (top->type != UCL_ARRAY) {
return NULL;
}
UCL_ARRAY_GET (vec, top);
if (vec != NULL && vec->n > 0 && index < vec->n) {
@ -3350,6 +3374,10 @@ ucl_array_find_index (const ucl_object_t *top, unsigned int index)
unsigned int
ucl_array_index_of (ucl_object_t *top, ucl_object_t *elt)
{
if (top->type != UCL_ARRAY) {
return (unsigned int)(-1);
}
UCL_ARRAY_GET (vec, top);
unsigned i;
@ -3370,6 +3398,10 @@ ucl_object_t *
ucl_array_replace_index (ucl_object_t *top, ucl_object_t *elt,
unsigned int index)
{
if (top->type != UCL_ARRAY) {
return NULL;
}
UCL_ARRAY_GET (vec, top);
ucl_object_t *ret = NULL;

4
src/client/rspamdclient.c

@ -1,5 +1,5 @@
/*
* Copyright 2024 Vsevolod Stakhov
* Copyright 2025 Vsevolod Stakhov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -231,7 +231,7 @@ rspamd_client_finish_handler(struct rspamd_http_connection *conn,
}
}
parser = ucl_parser_new(0);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_chunk_full(parser, start, len,
ucl_parser_get_default_priority(parser),
UCL_DUPLICATE_APPEND, UCL_PARSE_AUTO)) {

4
src/controller.c

@ -2311,7 +2311,7 @@ rspamd_controller_handle_saveactions(
return 0;
}
parser = ucl_parser_new(0);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_chunk(parser, msg->body_buf.begin, msg->body_buf.len)) {
if ((error = ucl_parser_get_error(parser)) != NULL) {
msg_err_session("cannot parse input: %s", error);
@ -2434,7 +2434,7 @@ rspamd_controller_handle_savesymbols(
return 0;
}
parser = ucl_parser_new(0);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_chunk(parser, msg->body_buf.begin, msg->body_buf.len)) {
if ((error = ucl_parser_get_error(parser)) != NULL) {
msg_err_session("cannot parse input: %s", error);

10
src/fuzzy_storage.c

@ -342,7 +342,7 @@ ucl_keymap_fin_cb(struct map_cb_data *data, void **target)
return;
}
parser = ucl_parser_new(UCL_PARSER_NO_FILEVARS);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_chunk(parser, jb->buf->str, jb->buf->len)) {
msg_err_config("cannot load ucl data: parse error %s",
@ -1339,7 +1339,8 @@ rspamd_fuzzy_check_callback(struct rspamd_fuzzy_reply *result, void *ud)
/* We push shingles merely for commands that modify content to avoid extra work */
if (is_shingle && cmd->cmd != FUZZY_CHECK) {
lua_newshingle(L, &session->cmd.sgl);
} else {
}
else {
lua_pushnil(L);
}
@ -1528,7 +1529,8 @@ rspamd_fuzzy_process_command(struct fuzzy_session *session)
/* We push shingles merely for commands that modify content to avoid extra work */
if (is_shingle && cmd->cmd != FUZZY_CHECK) {
lua_newshingle(L, &session->cmd.sgl);
} else {
}
else {
lua_pushnil(L);
}
@ -2663,7 +2665,7 @@ rspamd_fuzzy_maybe_load_ratelimits(struct rspamd_fuzzy_storage_ctx *ctx)
RSPAMD_DBDIR);
if (access(path, R_OK) != -1) {
struct ucl_parser *parser = ucl_parser_new(UCL_PARSER_NO_IMPLICIT_ARRAYS | UCL_PARSER_DISABLE_MACRO);
struct ucl_parser *parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (ucl_parser_add_file(parser, path)) {
ucl_object_t *obj = ucl_parser_get_object(parser);
int loaded = 0;

6
src/libmime/lang_detection.c

@ -1,5 +1,5 @@
/*
* Copyright 2024 Vsevolod Stakhov
* Copyright 2025 Vsevolod Stakhov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -363,7 +363,7 @@ rspamd_language_detector_read_file(struct rspamd_config *cfg,
double mean = 0, std = 0, delta = 0, delta2 = 0, m2 = 0;
enum rspamd_language_category cat = RSPAMD_LANGUAGE_MAX;
parser = ucl_parser_new(UCL_PARSER_NO_FILEVARS);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_file(parser, path)) {
msg_warn_config("cannot parse file %s: %s", path,
ucl_parser_get_error(parser));
@ -825,7 +825,7 @@ rspamd_language_detector_init(struct rspamd_config *cfg)
languages_pattern = g_string_sized_new(PATH_MAX);
rspamd_printf_gstring(languages_pattern, "%s/stop_words", languages_path);
parser = ucl_parser_new(UCL_PARSER_DEFAULT);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (ucl_parser_add_file(parser, languages_pattern->str)) {
stop_words = ucl_parser_get_object(parser);

2
src/libserver/cfg_rcl.cxx

@ -3640,7 +3640,7 @@ rspamd_config_parse_ucl(struct rspamd_config *cfg,
/* Try to load keyfile if available */
auto keyfile_name = fmt::format("{}.key", filename);
rspamd::util::raii_file::open(keyfile_name, O_RDONLY).map([&](const auto &keyfile) {
auto *kp_parser = ucl_parser_new(0);
auto *kp_parser = ucl_parser_new(UCL_PARSER_DEFAULT);
if (ucl_parser_add_fd(kp_parser, keyfile.get_fd())) {
auto *kp_obj = ucl_parser_get_object(kp_parser);

2
src/libserver/cfg_utils.cxx

@ -1363,7 +1363,7 @@ rspamd_ucl_fin_cb(struct map_cb_data *data, void **target)
}
/* New data available */
auto *parser = ucl_parser_new(0);
auto *parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_chunk(parser, (unsigned char *) cbdata->buf.data(),
cbdata->buf.size())) {
msg_err_config("cannot parse map %s: %s",

4
src/libserver/dynamic_cfg.c

@ -1,5 +1,5 @@
/*
* Copyright 2023 Vsevolod Stakhov
* Copyright 2025 Vsevolod Stakhov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -195,7 +195,7 @@ json_config_fin_cb(struct map_cb_data *data, void **target)
return;
}
parser = ucl_parser_new(0);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_chunk(parser, jb->buf->str, jb->buf->len)) {
msg_err("cannot load json data: parse error %s",

8
src/libserver/roll_history.c

@ -1,11 +1,11 @@
/*-
* Copyright 2016 Vsevolod Stakhov
/*
* Copyright 2025 Vsevolod Stakhov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@ -231,7 +231,7 @@ rspamd_roll_history_load(struct roll_history *history, const char *filename)
return FALSE;
}
parser = ucl_parser_new(0);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_fd(parser, fd)) {
msg_warn("cannot parse history file %s: %s", filename,

4
src/libserver/rspamd_control.c

@ -1,5 +1,5 @@
/*
* Copyright 2024 Vsevolod Stakhov
* Copyright 2025 Vsevolod Stakhov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -214,7 +214,7 @@ rspamd_control_write_reply(struct rspamd_control_session *session)
case RSPAMD_CONTROL_FUZZY_STAT:
if (elt->attached_fd != -1) {
/* We have some data to parse */
parser = ucl_parser_new(0);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
ucl_object_insert_key(cur,
ucl_object_fromint(
elt->reply.reply.fuzzy_stat.status),

2
src/libserver/symcache/symcache_impl.cxx

@ -274,7 +274,7 @@ auto symcache::load_items() -> bool
return false;
}
auto *parser = ucl_parser_new(0);
auto *parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
const auto *p = (const std::uint8_t *) (hdr + 1);
if (!ucl_parser_add_chunk(parser, p, cached_map->get_size() - sizeof(*hdr))) {

2
src/libserver/worker_util.c

@ -2138,7 +2138,7 @@ rspamd_controller_load_saved_stats(struct rspamd_main *rspamd_main,
return;
}
parser = ucl_parser_new(0);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_file(parser, cfg->stats_file)) {
msg_err_config("cannot parse controller stats from %s: %s",

2
src/lua/lua_cryptobox.c

@ -404,7 +404,7 @@ lua_cryptobox_keypair_load(lua_State *L)
if (lua_type(L, 1) == LUA_TSTRING) {
buf = luaL_checklstring(L, 1, &len);
if (buf != NULL) {
parser = ucl_parser_new(0);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_chunk(parser, buf, len)) {
msg_err("cannot open keypair from data: %s",

8
src/rspamadm/control.c

@ -1,11 +1,11 @@
/*-
* Copyright 2016 Vsevolod Stakhov
/*
* Copyright 2025 Vsevolod Stakhov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@ -112,7 +112,7 @@ rspamd_control_finish_handler(struct rspamd_http_connection *conn,
struct rspamadm_control_cbdata *cbdata = conn->ud;
body = rspamd_http_message_get_body(msg, &body_len);
parser = ucl_parser_new(0);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!body || !ucl_parser_add_chunk(parser, body, body_len)) {
rspamd_fprintf(stderr, "cannot parse server's reply: %s\n",

4
src/rspamadm/signtool.c

@ -1,5 +1,5 @@
/*
* Copyright 2024 Vsevolod Stakhov
* Copyright 2025 Vsevolod Stakhov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -573,7 +573,7 @@ rspamadm_signtool(int argc, char **argv, const struct rspamadm_command *cmd)
else {
g_assert(keypair_file != NULL);
parser = ucl_parser_new(0);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_file(parser, keypair_file) ||
(top = ucl_parser_get_object(parser)) == NULL) {

2
src/rspamd_proxy.c

@ -1012,7 +1012,7 @@ proxy_backend_parse_results(struct rspamd_proxy_session *session,
RSPAMD_FTOK_ASSIGN(&json_ct, "application/json");
if (ct && rspamd_ftok_casecmp(ct, &json_ct) == 0) {
parser = ucl_parser_new(0);
parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
if (!ucl_parser_add_chunk(parser, in, inlen)) {
char *encoded;

Loading…
Cancel
Save