mirror of
https://github.com/signalwire/freeswitch.git
synced 2026-07-24 21:22:09 +00:00
[Core, mod_commands] Interface allowlist
* [core] Add interface allowlist to gate module app/api registration Adds an optional, presence-activated allowlist in switch.conf.xml that controls which modules may register application / api / json_api / chat-application interfaces. With no <interface-allowlist> configured nothing is enforced; when at least one <allow> entry is present, only listed interfaces register at load time and all others are refused (the module still loads and switch_loadable_module_process still returns SUCCESS -- the blocked interface is simply never exposed). Entries match at three levels of precision: mod_commands - whole module mod_commands.system - any interface named "system" mod_commands.system.api - a specific type (app|api|json_api|chat_app) Enforcement lives in switch_loadable_module_process() so every module, at boot and at runtime `load`, is subject to the same policy. This gives operators a way to disable the "system"/"spawn" shell-exec API commands (and equivalents) system-wide. Also adds the `interface_allowlist_dump [modules] [plain]` API, which walks the loaded modules and prints their interfaces in the allowlist key format so the current state can be captured and pruned offline into config. * [mod_commands] Add tests for the interface allowlist New test_interface_allowlist boots the core with an active <interface-allowlist> (conf_interface_allowlist/) that permits only a couple of mod_commands interfaces, then loads mod_commands and verifies: - listed commands register and run (status, version) while unlisted and shell-exec commands are refused (system, spawn, uptime) -- refusal surfaces as switch_api_execute returning FALSE / command-not-found, with the command function never invoked; - a "module.name.type" entry gates by type: the API "status" loads while the JSON API of the same name stays blocked; - interface_allowlist_dump prints the config format in its xml, modules and plain variants, and reflects module capabilities (system appears in the dump even though it was blocked from registering). * [config] Fix interior -- in interface-allowlist comment breaking XML parse The explanatory comment used -- as em-dash pairs. The XML parser treats -- inside a comment as the comment close, causing an "unclosed <!--" error that prevents the whole freeswitch.xml from parsing (boot and reloadxml both fail). Replace the -- pairs with ordinary punctuation. * update .gitignore * [core] Warn when interface-allowlist section is present but parses no entries
This commit is contained in:
committed by
Andrey Volk
parent
0a54a48f37
commit
28f251f634
@@ -282,5 +282,6 @@ images/test_text.png
|
||||
|
||||
src/mod/codecs/mod_amrwb/test/test_amrwb
|
||||
src/mod/endpoints/mod_sofia/test/sipp-based-tests
|
||||
src/mod/applications/mod_commands/test/test_interface_allowlist
|
||||
|
||||
.DS_Store
|
||||
|
||||
@@ -208,4 +208,39 @@
|
||||
|
||||
</settings>
|
||||
|
||||
<!--
|
||||
Interface allowlist (optional security hardening).
|
||||
|
||||
When this section contains at least one <allow> entry, ONLY the interfaces listed here
|
||||
are permitted to register; every other application / api / json_api / chat-application
|
||||
from ANY module is refused at load time (the module still loads, it just does not expose
|
||||
the blocked interface). With this section absent or empty, nothing is enforced and all
|
||||
interfaces load normally.
|
||||
|
||||
An interface is permitted if it matches ANY entry. Entries may be written at three
|
||||
levels of precision:
|
||||
|
||||
<allow name="mod_commands"/> - permit every interface in mod_commands
|
||||
<allow name="mod_commands.status"/> - permit any interface named "status" (all types)
|
||||
<allow name="mod_commands.status.api"/> - permit ONLY the api named "status"
|
||||
|
||||
The optional trailing type token is one of: app, api, json_api, chat_app. This is how
|
||||
you permit, e.g., the APP named "system" while still blocking an API/JSON-API of the
|
||||
same name.
|
||||
|
||||
WARNING: this is an aggressive, system-wide allowlist. Once enabled you must list every
|
||||
module (or interface) whose apps/APIs you rely on, including management commands from
|
||||
mod_commands (load, unload, reload, status, ...) and your dialplan apps; otherwise the
|
||||
system will not function. It only affects app/api/json_api/chat-app registration; codecs,
|
||||
endpoints, timers, formats, etc. are never gated.
|
||||
-->
|
||||
<!--
|
||||
<interface-allowlist>
|
||||
<allow name="mod_commands"/>
|
||||
<allow name="mod_dptools"/>
|
||||
<allow name="mod_dialplan_xml"/>
|
||||
<allow name="mod_sofia"/>
|
||||
</interface-allowlist>
|
||||
-->
|
||||
|
||||
</configuration>
|
||||
|
||||
@@ -156,6 +156,14 @@ SWITCH_DECLARE(switch_status_t) switch_loadable_module_enumerate_available(const
|
||||
*/
|
||||
SWITCH_DECLARE(switch_status_t) switch_loadable_module_enumerate_loaded(switch_modulename_callback_func_t callback, void *user_data);
|
||||
|
||||
/*!
|
||||
\brief Dump loaded module application/api/json_api/chat-application interfaces in interface-allowlist key format
|
||||
\param stream stream to write the dump to
|
||||
\param by_module if true emit one entry per module, otherwise one fully qualified entry (module.name.type) per interface
|
||||
\param xml if true wrap entries as <allow/> tags inside an <interface-allowlist> element, otherwise emit raw keys
|
||||
*/
|
||||
SWITCH_DECLARE(void) switch_loadable_module_dump_interface_allowlist(switch_stream_handle_t *stream, switch_bool_t by_module, switch_bool_t xml);
|
||||
|
||||
/*!
|
||||
\brief build a dynamic module object and register it (for use in double embeded modules)
|
||||
\param filename the name of the modules source file
|
||||
|
||||
@@ -12,8 +12,13 @@ libmodcommands_la_SOURCES = $(mod_commands_la_SOURCES)
|
||||
libmodcommands_la_CFLAGS = $(mod_commands_la_CFLAGS)
|
||||
|
||||
noinst_PROGRAMS = test/test_mod_commands
|
||||
noinst_PROGRAMS += test/test_interface_allowlist
|
||||
test_test_mod_commands_CFLAGS = $(SWITCH_AM_CFLAGS) -I../ -DSWITCH_TEST_BASE_DIR_FOR_CONF=\"${abs_builddir}/test\" -DSWITCH_TEST_BASE_DIR_OVERRIDE=\"${abs_builddir}/test\"
|
||||
test_test_mod_commands_LDFLAGS = -avoid-version -no-undefined $(SWITCH_AM_LDFLAGS)
|
||||
test_test_mod_commands_LDADD = libmodcommands.la $(switch_builddir)/libfreeswitch.la
|
||||
|
||||
test_test_interface_allowlist_CFLAGS = $(SWITCH_AM_CFLAGS) -I../ -DSWITCH_TEST_BASE_DIR_FOR_CONF=\"${abs_builddir}/test\" -DSWITCH_TEST_BASE_DIR_OVERRIDE=\"${abs_builddir}/test\"
|
||||
test_test_interface_allowlist_LDFLAGS = -avoid-version -no-undefined $(SWITCH_AM_LDFLAGS)
|
||||
test_test_interface_allowlist_LDADD = libmodcommands.la $(switch_builddir)/libfreeswitch.la
|
||||
|
||||
TESTS = $(noinst_PROGRAMS)
|
||||
|
||||
@@ -6665,6 +6665,26 @@ SWITCH_STANDARD_API(bg_spawn_function)
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
#define INTERFACE_ALLOWLIST_DUMP_SYNTAX "[modules] [plain]"
|
||||
SWITCH_STANDARD_API(interface_allowlist_dump_function)
|
||||
{
|
||||
switch_bool_t by_module = SWITCH_FALSE;
|
||||
switch_bool_t xml = SWITCH_TRUE;
|
||||
|
||||
if (!zstr(cmd)) {
|
||||
if (switch_stristr("module", cmd)) {
|
||||
by_module = SWITCH_TRUE;
|
||||
}
|
||||
if (switch_stristr("plain", cmd) || switch_stristr("list", cmd)) {
|
||||
xml = SWITCH_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
switch_loadable_module_dump_interface_allowlist(stream, by_module, xml);
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
SWITCH_STANDARD_API(strftime_tz_api_function)
|
||||
{
|
||||
char *format = NULL;
|
||||
@@ -7616,6 +7636,11 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_commands_load)
|
||||
SWITCH_ADD_API(commands_api_interface, "spawn_stream", "Execute a spawn command and capture it's output", spawn_stream_function, SPAWN_SYNTAX);
|
||||
}
|
||||
|
||||
SWITCH_ADD_API(commands_api_interface, "interface_allowlist_dump", "Dump loaded interfaces in interface-allowlist config format", interface_allowlist_dump_function, INTERFACE_ALLOWLIST_DUMP_SYNTAX);
|
||||
switch_console_set_complete("add interface_allowlist_dump");
|
||||
switch_console_set_complete("add interface_allowlist_dump modules");
|
||||
switch_console_set_complete("add interface_allowlist_dump plain");
|
||||
|
||||
SWITCH_ADD_API(commands_api_interface, "acl", "Compare an ip to an acl list", acl_function, "<ip> <list_name>");
|
||||
SWITCH_ADD_API(commands_api_interface, "alias", "Alias", alias_function, ALIAS_SYNTAX); SWITCH_ADD_API(commands_api_interface, "coalesce", "Return first nonempty parameter", coalesce_function, COALESCE_SYNTAX);
|
||||
SWITCH_ADD_API(commands_api_interface, "banner", "Return the system banner", banner_function, "");
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0"?>
|
||||
<document type="freeswitch/xml">
|
||||
|
||||
<section name="configuration" description="Various Configuration">
|
||||
<configuration name="modules.conf" description="Modules">
|
||||
<modules>
|
||||
<load module="mod_console"/>
|
||||
<load module="mod_loopback"/>
|
||||
<load module="mod_sndfile"/>
|
||||
</modules>
|
||||
</configuration>
|
||||
|
||||
<configuration name="console.conf" description="Console Logger">
|
||||
<mappings>
|
||||
<map name="all" value="console,debug,info,notice,warning,err,crit,alert"/>
|
||||
</mappings>
|
||||
<settings>
|
||||
<param name="colorize" value="true"/>
|
||||
<param name="loglevel" value="debug"/>
|
||||
</settings>
|
||||
</configuration>
|
||||
|
||||
<configuration name="switch.conf" description="Core Configuration">
|
||||
<!--
|
||||
Active interface allowlist used by the test. Because at least one <allow> entry
|
||||
is present, enforcement is ON: only the interfaces below may register.
|
||||
-->
|
||||
<interface-allowlist>
|
||||
<!-- Autoloaded helper modules permitted wholesale. -->
|
||||
<allow name="mod_console"/>
|
||||
<allow name="mod_loopback"/>
|
||||
<allow name="mod_sndfile"/>
|
||||
|
||||
<!-- Type-qualified: only the API named "status" (the JSON API "status" stays blocked). -->
|
||||
<allow name="mod_commands.status.api"/>
|
||||
<!-- Name only: any type named "version". -->
|
||||
<allow name="mod_commands.version"/>
|
||||
<!-- Needed so the test can invoke the dump API itself. -->
|
||||
<allow name="mod_commands.interface_allowlist_dump"/>
|
||||
</interface-allowlist>
|
||||
</configuration>
|
||||
|
||||
<configuration name="timezones.conf" description="Timezones">
|
||||
<timezones>
|
||||
<zone name="GMT" value="GMT0" />
|
||||
</timezones>
|
||||
</configuration>
|
||||
</section>
|
||||
|
||||
</document>
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
|
||||
* Copyright (C) 2005-2024, Anthony Minessale II <anthm@freeswitch.org>
|
||||
*
|
||||
* Version: MPL 1.1
|
||||
*
|
||||
* The contents of this file are subject to the Mozilla Public License Version
|
||||
* 1.1 (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.mozilla.org/MPL/
|
||||
*
|
||||
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing rights and limitations under the
|
||||
* License.
|
||||
*
|
||||
* The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
|
||||
*
|
||||
* Contributor(s):
|
||||
* Chris Rienzo <chris@signalwire.com>
|
||||
*
|
||||
* test_interface_allowlist -- tests the switch.conf.xml <interface-allowlist>
|
||||
*
|
||||
* The core is booted with conf_interface_allowlist/freeswitch.xml, whose allowlist permits only
|
||||
* a couple of mod_commands interfaces. mod_commands is then loaded and we verify that unlisted /
|
||||
* dangerous commands are refused registration while listed ones load, that a "module.name.type"
|
||||
* entry gates by interface type, and that interface_allowlist_dump prints the config format.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <test/switch_test.h>
|
||||
#include <string.h>
|
||||
|
||||
FST_CORE_BEGIN("conf_interface_allowlist")
|
||||
{
|
||||
FST_MODULE_BEGIN(mod_commands, interface_allowlist_test)
|
||||
{
|
||||
FST_SETUP_BEGIN()
|
||||
{
|
||||
}
|
||||
FST_SETUP_END()
|
||||
|
||||
/* Listed commands register and run; unlisted (including the shell-exec commands) are refused.
|
||||
A refused command is not in the api hash, so switch_api_execute returns FALSE without ever
|
||||
invoking the command function (empty arg means system/spawn would only print usage anyway). */
|
||||
FST_TEST_BEGIN(allowlist_blocks_unlisted_apis)
|
||||
{
|
||||
switch_stream_handle_t stream = { 0 };
|
||||
|
||||
/* allowed: mod_commands.status.api */
|
||||
SWITCH_STANDARD_STREAM(stream);
|
||||
fst_check(switch_api_execute("status", "", NULL, &stream) == SWITCH_STATUS_SUCCESS);
|
||||
switch_safe_free(stream.data);
|
||||
|
||||
/* allowed: mod_commands.version */
|
||||
SWITCH_STANDARD_STREAM(stream);
|
||||
fst_check(switch_api_execute("version", "", NULL, &stream) == SWITCH_STATUS_SUCCESS);
|
||||
switch_safe_free(stream.data);
|
||||
|
||||
/* blocked: the shell-exec API this whole feature exists to disable */
|
||||
SWITCH_STANDARD_STREAM(stream);
|
||||
fst_check(switch_api_execute("system", "", NULL, &stream) == SWITCH_STATUS_FALSE);
|
||||
switch_safe_free(stream.data);
|
||||
|
||||
/* blocked: spawn family */
|
||||
SWITCH_STANDARD_STREAM(stream);
|
||||
fst_check(switch_api_execute("spawn", "", NULL, &stream) == SWITCH_STATUS_FALSE);
|
||||
switch_safe_free(stream.data);
|
||||
|
||||
/* blocked: an ordinary unlisted command, proving default-deny once the list is active */
|
||||
SWITCH_STANDARD_STREAM(stream);
|
||||
fst_check(switch_api_execute("uptime", "", NULL, &stream) == SWITCH_STATUS_FALSE);
|
||||
switch_safe_free(stream.data);
|
||||
}
|
||||
FST_TEST_END()
|
||||
|
||||
/* "mod_commands.status.api" permits only the API type; the JSON API of the same name stays blocked. */
|
||||
FST_TEST_BEGIN(allowlist_type_qualified_entry)
|
||||
{
|
||||
switch_stream_handle_t stream = { 0 };
|
||||
cJSON *jcmd, *reply = NULL;
|
||||
switch_status_t status;
|
||||
|
||||
/* API "status" is permitted. */
|
||||
SWITCH_STANDARD_STREAM(stream);
|
||||
fst_check(switch_api_execute("status", "", NULL, &stream) == SWITCH_STATUS_SUCCESS);
|
||||
switch_safe_free(stream.data);
|
||||
|
||||
/* JSON API "status" is NOT permitted (only the .api type was allowed) -> not found. */
|
||||
jcmd = cJSON_Parse("{\"command\":\"status\",\"data\":\"\"}");
|
||||
fst_requires(jcmd);
|
||||
status = switch_json_api_execute(jcmd, NULL, &reply);
|
||||
fst_check(status == SWITCH_STATUS_FALSE);
|
||||
if (reply) {
|
||||
cJSON_Delete(reply);
|
||||
}
|
||||
cJSON_Delete(jcmd);
|
||||
}
|
||||
FST_TEST_END()
|
||||
|
||||
/* interface_allowlist_dump emits keys in the config format. It reflects module capabilities
|
||||
(every interface the module registered), independent of what enforcement blocked -- that is
|
||||
what makes it useful for generating a starting allowlist. */
|
||||
FST_TEST_BEGIN(interface_allowlist_dump_format)
|
||||
{
|
||||
switch_stream_handle_t stream = { 0 };
|
||||
|
||||
/* Default: XML, one fully qualified entry per interface. */
|
||||
SWITCH_STANDARD_STREAM(stream);
|
||||
fst_check(switch_api_execute("interface_allowlist_dump", "", NULL, &stream) == SWITCH_STATUS_SUCCESS);
|
||||
fst_requires(stream.data);
|
||||
fst_check(strstr((char *) stream.data, "<interface-allowlist>") != NULL);
|
||||
fst_check(strstr((char *) stream.data, "mod_commands.status.api") != NULL);
|
||||
fst_check(strstr((char *) stream.data, "mod_commands.status.json_api") != NULL);
|
||||
/* system is present in the dump even though it was blocked from registering. */
|
||||
fst_check(strstr((char *) stream.data, "mod_commands.system.api") != NULL);
|
||||
switch_safe_free(stream.data);
|
||||
|
||||
/* "modules" variant: one entry per module. */
|
||||
SWITCH_STANDARD_STREAM(stream);
|
||||
fst_check(switch_api_execute("interface_allowlist_dump", "modules", NULL, &stream) == SWITCH_STATUS_SUCCESS);
|
||||
fst_requires(stream.data);
|
||||
fst_check(strstr((char *) stream.data, "<allow name=\"mod_commands\"/>") != NULL);
|
||||
switch_safe_free(stream.data);
|
||||
|
||||
/* "plain" variant: raw keys, no XML wrapper. */
|
||||
SWITCH_STANDARD_STREAM(stream);
|
||||
fst_check(switch_api_execute("interface_allowlist_dump", "plain", NULL, &stream) == SWITCH_STATUS_SUCCESS);
|
||||
fst_requires(stream.data);
|
||||
fst_check(strstr((char *) stream.data, "<interface-allowlist>") == NULL);
|
||||
fst_check(strstr((char *) stream.data, "mod_commands.version.api") != NULL);
|
||||
switch_safe_free(stream.data);
|
||||
}
|
||||
FST_TEST_END()
|
||||
|
||||
FST_TEARDOWN_BEGIN()
|
||||
{
|
||||
}
|
||||
FST_TEARDOWN_END()
|
||||
}
|
||||
FST_MODULE_END()
|
||||
}
|
||||
FST_CORE_END()
|
||||
@@ -98,6 +98,8 @@ struct switch_loadable_module_container {
|
||||
switch_hash_t *limit_hash;
|
||||
switch_hash_t *database_hash;
|
||||
switch_hash_t *secondary_recover_hash;
|
||||
switch_hash_t *interface_allowlist;
|
||||
switch_bool_t interface_allowlist_enabled;
|
||||
switch_mutex_t *mutex;
|
||||
switch_thread_rwlock_t *chat_rwlock;
|
||||
switch_memory_pool_t *pool;
|
||||
@@ -153,6 +155,57 @@ static void switch_loadable_module_runtime(void)
|
||||
switch_mutex_unlock(loadable_modules.mutex);
|
||||
}
|
||||
|
||||
/*
|
||||
When an interface allowlist is configured in switch.conf.xml, only interfaces that are
|
||||
explicitly permitted are allowed to register. An <allow> entry may be written at three
|
||||
levels of precision (an interface is permitted if it matches ANY configured entry):
|
||||
|
||||
"mod_commands" - every interface in the module
|
||||
"mod_commands.system" - any interface named "system" (type omitted matches all)
|
||||
"mod_commands.system.api" - only the given interface TYPE named "system"
|
||||
|
||||
The optional trailing type token is one of: "app" (dialplan application), "api",
|
||||
"json_api", "chat_app" (chat application). This lets you permit, say, the APP named
|
||||
"system" while still blocking an API or JSON API of the same name. With no allowlist
|
||||
configured, everything is permitted (nothing is enforced).
|
||||
*/
|
||||
static switch_bool_t switch_loadable_module_interface_allowed(const char *modname, const char *name, const char *type)
|
||||
{
|
||||
char full[512];
|
||||
|
||||
/* No allowlist configured: enforce nothing. */
|
||||
if (!loadable_modules.interface_allowlist_enabled) {
|
||||
return SWITCH_TRUE;
|
||||
}
|
||||
|
||||
if (zstr(modname)) {
|
||||
return SWITCH_TRUE;
|
||||
}
|
||||
|
||||
/* Whole module permitted, e.g. "mod_commands". */
|
||||
if (switch_core_hash_find(loadable_modules.interface_allowlist, modname)) {
|
||||
return SWITCH_TRUE;
|
||||
}
|
||||
|
||||
if (!zstr(name)) {
|
||||
/* Any type of this name permitted, e.g. "mod_commands.system". */
|
||||
switch_snprintf(full, sizeof(full), "%s.%s", modname, name);
|
||||
if (switch_core_hash_find(loadable_modules.interface_allowlist, full)) {
|
||||
return SWITCH_TRUE;
|
||||
}
|
||||
|
||||
/* Only this specific type of this name permitted, e.g. "mod_commands.system.api". */
|
||||
if (!zstr(type)) {
|
||||
switch_snprintf(full, sizeof(full), "%s.%s.%s", modname, name, type);
|
||||
if (switch_core_hash_find(loadable_modules.interface_allowlist, full)) {
|
||||
return SWITCH_TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SWITCH_FALSE;
|
||||
}
|
||||
|
||||
static switch_status_t switch_loadable_module_process(char *key, switch_loadable_module_t *new_module, switch_hash_t *event_hash)
|
||||
{
|
||||
switch_event_t *event;
|
||||
@@ -325,6 +378,8 @@ static switch_status_t switch_loadable_module_process(char *key, switch_loadable
|
||||
for (ptr = new_module->module_interface->application_interface; ptr; ptr = ptr->next) {
|
||||
if (!ptr->interface_name) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "Failed to load application interface from %s due to no interface name.\n", key);
|
||||
} else if (!switch_loadable_module_interface_allowed(key, ptr->interface_name, "app")) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Skipping Application '%s' from %s: not permitted by interface allowlist\n", ptr->interface_name, key);
|
||||
} else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Adding Application '%s'\n", ptr->interface_name);
|
||||
if (switch_event_create(&event, SWITCH_EVENT_MODULE_LOAD) == SWITCH_STATUS_SUCCESS) {
|
||||
@@ -355,6 +410,8 @@ static switch_status_t switch_loadable_module_process(char *key, switch_loadable
|
||||
for (ptr = new_module->module_interface->chat_application_interface; ptr; ptr = ptr->next) {
|
||||
if (!ptr->interface_name) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "Failed to load application interface from %s due to no interface name.\n", key);
|
||||
} else if (!switch_loadable_module_interface_allowed(key, ptr->interface_name, "chat_app")) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Skipping Chat Application '%s' from %s: not permitted by interface allowlist\n", ptr->interface_name, key);
|
||||
} else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Adding Chat Application '%s'\n", ptr->interface_name);
|
||||
if (switch_event_create(&event, SWITCH_EVENT_MODULE_LOAD) == SWITCH_STATUS_SUCCESS) {
|
||||
@@ -385,6 +442,8 @@ static switch_status_t switch_loadable_module_process(char *key, switch_loadable
|
||||
for (ptr = new_module->module_interface->api_interface; ptr; ptr = ptr->next) {
|
||||
if (!ptr->interface_name) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "Failed to load api interface from %s due to no interface name.\n", key);
|
||||
} else if (!switch_loadable_module_interface_allowed(key, ptr->interface_name, "api")) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Skipping API '%s' from %s: not permitted by interface allowlist\n", ptr->interface_name, key);
|
||||
} else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Adding API Function '%s'\n", ptr->interface_name);
|
||||
if (switch_event_create(&event, SWITCH_EVENT_MODULE_LOAD) == SWITCH_STATUS_SUCCESS) {
|
||||
@@ -415,6 +474,8 @@ static switch_status_t switch_loadable_module_process(char *key, switch_loadable
|
||||
for (ptr = new_module->module_interface->json_api_interface; ptr; ptr = ptr->next) {
|
||||
if (!ptr->interface_name) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_CRIT, "Failed to load JSON api interface from %s due to no interface name.\n", key);
|
||||
} else if (!switch_loadable_module_interface_allowed(key, ptr->interface_name, "json_api")) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Skipping JSON API '%s' from %s: not permitted by interface allowlist\n", ptr->interface_name, key);
|
||||
} else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Adding JSON API Function '%s'\n", ptr->interface_name);
|
||||
if (switch_event_create(&event, SWITCH_EVENT_MODULE_LOAD) == SWITCH_STATUS_SUCCESS) {
|
||||
@@ -1984,6 +2045,91 @@ SWITCH_DECLARE(switch_status_t) switch_loadable_module_enumerate_loaded(switch_m
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
static void dump_allowlist_entry(switch_stream_handle_t *stream, switch_bool_t xml, const char *modname, const char *name, const char *type)
|
||||
{
|
||||
if (xml) {
|
||||
stream->write_function(stream, " <allow name=\"%s.%s.%s\"/>\n", modname, name, type);
|
||||
} else {
|
||||
stream->write_function(stream, "%s.%s.%s\n", modname, name, type);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Dump every loaded module's application / api / json_api / chat-application interfaces in the
|
||||
interface-allowlist key format, so the current state can be captured and pruned offline into a
|
||||
switch.conf.xml <interface-allowlist> section. Keys use module->key (the same name enforcement
|
||||
matches on). by_module emits one entry per module ("mod_commands"); otherwise one fully
|
||||
qualified entry per interface ("mod_commands.system.api"). xml wraps entries as <allow/> tags.
|
||||
*/
|
||||
SWITCH_DECLARE(void) switch_loadable_module_dump_interface_allowlist(switch_stream_handle_t *stream, switch_bool_t by_module, switch_bool_t xml)
|
||||
{
|
||||
switch_hash_index_t *hi;
|
||||
void *val;
|
||||
switch_loadable_module_t *module;
|
||||
|
||||
if (xml) {
|
||||
stream->write_function(stream, "<!-- generated by 'interface_allowlist_dump'; prune entries you want to block, then paste into switch.conf.xml -->\n");
|
||||
stream->write_function(stream, "<interface-allowlist>\n");
|
||||
}
|
||||
|
||||
switch_mutex_lock(loadable_modules.mutex);
|
||||
for (hi = switch_core_hash_first(loadable_modules.module_hash); hi; hi = switch_core_hash_next(&hi)) {
|
||||
switch_core_hash_this(hi, NULL, NULL, &val);
|
||||
module = (switch_loadable_module_t *) val;
|
||||
|
||||
if (!module || zstr(module->key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (by_module) {
|
||||
if (xml) {
|
||||
stream->write_function(stream, " <allow name=\"%s\"/>\n", module->key);
|
||||
} else {
|
||||
stream->write_function(stream, "%s\n", module->key);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!module->module_interface) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (xml) {
|
||||
stream->write_function(stream, " <!-- %s -->\n", module->key);
|
||||
}
|
||||
|
||||
{
|
||||
const switch_application_interface_t *ptr;
|
||||
for (ptr = module->module_interface->application_interface; ptr; ptr = ptr->next) {
|
||||
if (ptr->interface_name) dump_allowlist_entry(stream, xml, module->key, ptr->interface_name, "app");
|
||||
}
|
||||
}
|
||||
{
|
||||
const switch_api_interface_t *ptr;
|
||||
for (ptr = module->module_interface->api_interface; ptr; ptr = ptr->next) {
|
||||
if (ptr->interface_name) dump_allowlist_entry(stream, xml, module->key, ptr->interface_name, "api");
|
||||
}
|
||||
}
|
||||
{
|
||||
const switch_json_api_interface_t *ptr;
|
||||
for (ptr = module->module_interface->json_api_interface; ptr; ptr = ptr->next) {
|
||||
if (ptr->interface_name) dump_allowlist_entry(stream, xml, module->key, ptr->interface_name, "json_api");
|
||||
}
|
||||
}
|
||||
{
|
||||
const switch_chat_application_interface_t *ptr;
|
||||
for (ptr = module->module_interface->chat_application_interface; ptr; ptr = ptr->next) {
|
||||
if (ptr->interface_name) dump_allowlist_entry(stream, xml, module->key, ptr->interface_name, "chat_app");
|
||||
}
|
||||
}
|
||||
}
|
||||
switch_mutex_unlock(loadable_modules.mutex);
|
||||
|
||||
if (xml) {
|
||||
stream->write_function(stream, "</interface-allowlist>\n");
|
||||
}
|
||||
}
|
||||
|
||||
SWITCH_DECLARE(switch_status_t) switch_loadable_module_build_dynamic(char *filename,
|
||||
switch_module_load_t switch_module_load,
|
||||
switch_module_runtime_t switch_module_runtime,
|
||||
@@ -2136,7 +2282,46 @@ SWITCH_DECLARE(switch_status_t) switch_loadable_module_init(switch_bool_t autolo
|
||||
switch_core_hash_init(&loadable_modules.secondary_recover_hash);
|
||||
switch_mutex_init(&loadable_modules.mutex, SWITCH_MUTEX_NESTED, loadable_modules.pool);
|
||||
switch_thread_rwlock_create(&loadable_modules.chat_rwlock, loadable_modules.pool);
|
||||
|
||||
|
||||
/*
|
||||
Build the optional interface allowlist from switch.conf.xml. When at least one <allow>
|
||||
entry is present, ONLY the permitted modules (e.g. "mod_commands") or specific interfaces
|
||||
(e.g. "mod_commands.system") may register application/api/json_api/chat-application
|
||||
interfaces; every other such interface is refused at load time. With no allowlist
|
||||
configured, nothing is enforced (all interfaces load as before). Built before the
|
||||
autoload check so runtime module loads are subject to the same policy.
|
||||
*/
|
||||
switch_core_hash_init_nocase(&loadable_modules.interface_allowlist);
|
||||
loadable_modules.interface_allowlist_enabled = SWITCH_FALSE;
|
||||
{
|
||||
switch_xml_t cfg_al = NULL, xml_al = NULL, list = NULL, item = NULL;
|
||||
if ((xml_al = switch_xml_open_cfg("switch.conf", &cfg_al, NULL))) {
|
||||
if ((list = switch_xml_child(cfg_al, "interface-allowlist"))) {
|
||||
for (item = switch_xml_child(list, "allow"); item; item = item->next) {
|
||||
const char *aname = switch_xml_attr_soft(item, "name");
|
||||
if (!zstr(aname)) {
|
||||
char *entry = switch_core_strdup(loadable_modules.pool, aname);
|
||||
switch_core_hash_insert(loadable_modules.interface_allowlist, entry, entry);
|
||||
loadable_modules.interface_allowlist_enabled = SWITCH_TRUE;
|
||||
}
|
||||
}
|
||||
if (!loadable_modules.interface_allowlist_enabled) {
|
||||
/* Section present but nothing usable parsed (e.g. a typo'd tag or missing name= attribute).
|
||||
Enforcement is boot-only, so a silent miss would leave the box unhardened with no runtime
|
||||
safety net; make the misconfiguration loud instead of failing open quietly. */
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR,
|
||||
"Interface allowlist section <interface-allowlist> is present but no valid <allow name=\"...\"/> "
|
||||
"entries were found; enforcement is DISABLED. Check for typos (lowercase <allow> with a name= attribute).\n");
|
||||
}
|
||||
}
|
||||
switch_xml_free(xml_al);
|
||||
}
|
||||
if (loadable_modules.interface_allowlist_enabled) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING,
|
||||
"Interface allowlist ACTIVE: only explicitly permitted application/api/json_api interfaces will load.\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (!autoload) return SWITCH_STATUS_SUCCESS;
|
||||
|
||||
/*
|
||||
@@ -2472,6 +2657,7 @@ SWITCH_DECLARE(void) switch_loadable_module_shutdown(void)
|
||||
switch_core_hash_destroy(&loadable_modules.database_hash);
|
||||
switch_core_hash_destroy(&loadable_modules.dialplan_hash);
|
||||
switch_core_hash_destroy(&loadable_modules.secondary_recover_hash);
|
||||
switch_core_hash_destroy(&loadable_modules.interface_allowlist);
|
||||
|
||||
switch_core_destroy_memory_pool(&loadable_modules.pool);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user