#define RSPAMD_LUA_HTTP_FLAG_RESOLVED (1 << 2)
#define RSPAMD_LUA_HTTP_FLAG_KEEP_ALIVE (1 << 3)
#define RSPAMD_LUA_HTTP_FLAG_YIELDED (1 << 4)
+#define RSPAMD_LUA_HTTP_FLAG_NO_LOCAL (1 << 5)
struct lua_http_cbdata {
struct rspamd_http_connection *conn;
static gboolean
lua_http_make_connection(struct lua_http_cbdata *cbd)
{
+ if (cbd->flags & RSPAMD_LUA_HTTP_FLAG_NO_LOCAL) {
+ struct rspamd_config *cfg = cbd->cfg;
+
+ if (cfg == NULL && cbd->task) {
+ cfg = cbd->task->cfg;
+ }
+
+ if (rspamd_ip_is_local_cfg(cfg, cbd->addr)) {
+ msg_info("forbid HTTP request to local address %s (host: %s) as 'forbid_local' is set",
+ rspamd_inet_address_to_string(cbd->addr),
+ cbd->host ? cbd->host : "unknown");
+
+ return FALSE;
+ }
+ }
+
rspamd_inet_address_set_port(cbd->addr, cbd->msg->port);
unsigned http_opts = RSPAMD_HTTP_CLIENT_SIMPLE;
* @param {resolver} resolver to perform DNS-requests. Usually got from either `task` or `config`
* @param {boolean} gzip if true, body of the requests will be compressed
* @param {boolean} no_ssl_verify disable SSL peer checks
+ * @param {boolean} forbid_local refuse to connect when the destination resolves to a loopback, link-local or `local_addrs` address (SSRF protection for requests to untrusted URLs)
* @param {boolean} keepalive enable keep-alive pool
* @param {string} user for HTTP authentication
* @param {string} password for HTTP authentication, only if "user" present
lua_pop(L, 1);
+ lua_pushstring(L, "forbid_local");
+ lua_gettable(L, 1);
+
+ if (!!lua_toboolean(L, -1)) {
+ flags |= RSPAMD_LUA_HTTP_FLAG_NO_LOCAL;
+ }
+
+ lua_pop(L, 1);
+
lua_pushstring(L, "keepalive");
lua_gettable(L, 1);
--proxy = "http://example.com:3128", -- send request through proxy, not yet implemented
key_prefix = 'rdr:', -- default hash name
check_ssl = false, -- check ssl certificates
+ -- SSRF protection: message URLs and their redirect targets are attacker
+ -- controlled, so never follow them into loopback, link-local or
+ -- local_addrs networks (e.g. cloud metadata services). Disable only if
+ -- internal redirectors must be resolved.
+ forbid_local = true,
max_urls = 5, -- how many urls to check (CTA checked in first place)
max_size = 10 * 1024, -- maximum body to process
-- Optional operator override. When set (a string, or a list of
max_size = settings.max_size,
opaque_body = true,
no_ssl_verify = not settings.check_ssl,
+ forbid_local = settings.forbid_local,
callback = http_callback,
}
end
apply_http_timeout(http_params)
- rspamd_http.request(http_params)
+ if not rspamd_http.request(http_params) then
+ -- Synchronous refusal (e.g. forbid_local blocked a numeric-IP URL):
+ -- the callback will never fire, so terminate the walk here
+ rspamd_logger.infox(task, 'cannot start HTTP request for %s', url)
+ chain_append(chain, url)
+ finalize_chain(task, chain, nil)
+ end
end
-- Top-level entry: walk the cached chain from orig_url, then either
... Settings={symbols_enabled = [LARGE_HTTP_TEST]}
Expect Symbol HTTP_SSL_LARGE
+Forbid local addresses
+ Scan File ${MESSAGE} Url=/request Method=get
+ ... Settings={symbols_enabled = [FORBID_LOCAL_TEST]}
+ Expect Symbols HTTP_FORBID_NUMERIC_DENIED HTTP_FORBID_DNS_ERROR
+
*** Keywords ***
Http Setup
Run Dummy Http
}
url_redirector {
redirector_hosts_map = "{= env.TESTDIR =}/configs/maps/redir.map";
+ # Test redirect targets live on 127.0.0.1
+ forbid_local = false;
}
url_redirector {
redirector_hosts_map = "{= env.TESTDIR =}/configs/maps/redir.map";
+ # Test redirect targets live on 127.0.0.1
+ forbid_local = false;
save_intermediate_redirs = {
redirectors = false;
non_redirectors = true;
url_redirector {
redirector_hosts_map = "{= env.TESTDIR =}/configs/maps/redir.map";
+ # Test redirect targets live on 127.0.0.1
+ forbid_local = false;
save_intermediate_redirs = {
redirectors = true;
non_redirectors = false;
url_redirector {
redirector_hosts_map = "{= env.TESTDIR =}/configs/maps/redir.map";
+ # Test redirect targets live on 127.0.0.1
+ forbid_local = false;
redirector_symbol = "URL_REDIRECTOR";
}
flags = 'coro'
})
+local function http_forbid_local_symbol(task)
+ -- Numeric loopback destination: denied synchronously, the callback
+ -- never fires and request() returns false
+ local ret = rspamd_http.request({
+ url = string.format('http://127.0.0.1:%d/request', http_port),
+ task = task,
+ method = 'get',
+ callback = function(err, code)
+ if err then
+ task:insert_result('HTTP_FORBID_NUMERIC_ERROR', 1.0, err)
+ else
+ task:insert_result('HTTP_FORBID_NUMERIC_' .. code, 1.0)
+ end
+ end,
+ timeout = 1,
+ forbid_local = true,
+ })
+ if ret == false then
+ task:insert_result('HTTP_FORBID_NUMERIC_DENIED', 1.0)
+ end
+
+ -- DNS-resolved loopback destination: denied after resolution, surfaces
+ -- as an asynchronous error
+ local err, response = rspamd_http.request({
+ url = string.format('http://site.resolveme:%d/request', http_port),
+ task = task,
+ method = 'get',
+ timeout = 1,
+ forbid_local = true,
+ })
+ if err then
+ task:insert_result('HTTP_FORBID_DNS_ERROR', 1.0, err)
+ else
+ task:insert_result('HTTP_FORBID_DNS_' .. response.code, 1.0)
+ end
+end
+rspamd_config:register_symbol({
+ name = 'FORBID_LOCAL_TEST',
+ score = 1.0,
+ callback = http_forbid_local_symbol,
+ no_squeeze = true,
+ flags = 'coro'
+})
+
rspamd_config:register_finish_script(finish)
rspamd_config:add_on_load(function(cfg, ev_base, worker)