Skip to content

[Core, mod_commands] Interface allowlist#3086

Open
andywolk wants to merge 1 commit into
masterfrom
allowlist
Open

[Core, mod_commands] Interface allowlist#3086
andywolk wants to merge 1 commit into
masterfrom
allowlist

Conversation

@andywolk

@andywolk andywolk commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Add an interface allowlist to control which module apps/APIs load

Motivation

FreeSWITCH's system, bg_system, spawn, and bgsystem commands execute arbitrary shell/binaries, and they're reachable from every API dispatch surface — the event socket,
XML-RPC, the fsapi/execute JSON gateways, scripting languages, and the dialplan. On a hardened or multi-tenant deployment that's a large, awkward-to-close attack surface,
and disabling it piecemeal (per module, per transport) is error-prone.

This adds a single, global mechanism to declare exactly which module interfaces a FreeSWITCH instance is allowed to expose. Anything not on the list is never registered,
so it cannot be dispatched from any surface.

What it does

A new optional section in switch.conf.xml gates the registration of application, api, json_api, and chat_application interfaces for every module,
enforced centrally in switch_loadable_module_process() (so it applies at boot and to runtime load).

  • Presence-activated, allow-only. With no (or an empty one), nothing is enforced and everything loads as before — fully backward compatible. As soon
    as one entry exists, enforcement is on and anything not listed is refused.
  • Load-time, not runtime. A refused interface is simply never inserted into the dispatch hashes. The owning module still loads normally and
    switch_loadable_module_process() still returns success — the interface just isn't exposed. This is more airtight than a per-call deny (the function pointer never enters
    the hash) and has no partial-init side effects, since the check runs after the module's load function has already returned.
  • Three levels of precision — an interface is permitted if it matches any entry:
    • mod_commands — every interface in the module
    • mod_commands.system — any interface named system (all types)
    • mod_commands.system.api — only that interface type named system (types: app, api, json_api, chat_app)

This lets you, for example, permit the dialplan system app while still blocking the system API.

Companion API for building the config

interface_allowlist_dump [modules] [plain] (mod_commands) walks the loaded modules and prints their interfaces in the allowlist key format, so you can capture the current
state and prune it offline:

  • default: XML <allow .../> entries, one fully-qualified module.name.type per interface, wrapped in
  • modules: one per module
  • plain: raw keys, no XML wrapper (for scripting)

The dump reflects module capabilities (everything a module registered), independent of what enforcement blocked — that's what makes it a useful starting point. Intended
workflow: boot with no allowlist → run the dump → delete the entries you want blocked → paste into switch.conf.xml → restart.

Example

Lock the box down to a known set, minus the shell-exec commands:

...

The vanilla switch.conf.xml ships the section commented out with documentation and a WARNING that enabling it is aggressive and system-wide (you must list every
module/interface you rely on, including management commands and dialplan apps).
Scope / limitations

This gates interface registration only, so it covers the system/spawn APIs, the system/bgsystem dialplan apps, and any other module command. It intentionally does not
touch:

  • codecs, endpoints, timers, formats, and dialplan-parser interfaces (gating those would break media/routing, not restrict command surface);
  • shell-exec paths that aren't interface registrations — , native scripting exec (os.execute, etc.), the mod_v8 system() global, and the raw
    system() spool-move in switch_core_file.c. Those need separate mitigation (e.g. gating the switch_system/switch_spawn core primitives).

Testing

New test_interface_allowlist FST test (in mod_commands/test, with its own conf_interface_allowlist/) boots the core with an active allowlist, loads mod_commands, and
verifies:

  • listed commands register and run (status, version) while unlisted / shell-exec commands are refused (system, spawn, uptime) — refusal surfaces as switch_api_execute()
    returning FALSE with the command function never invoked;
  • a module.name.type entry gates by type — the status API loads while the status JSON API stays blocked;
  • interface_allowlist_dump produces the expected xml / modules / plain output.

@andywolk
andywolk requested a review from morbit85 July 16, 2026 19:28
@andywolk andywolk added the enhancement New feature or request label Jul 16, 2026
@andywolk
andywolk requested a review from crienzo July 17, 2026 20:57
* [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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants