The FU-A (type 28) handler read the 2-byte FU header and copied
`datalen - 2` bytes into `fua_buf` without confirming the frame held
those header bytes. On a 1-byte frame `datalen - 2` wraps to a huge
unsigned length passed to `switch_buffer_write`, and `q[1]` is read
past the payload. Reject FU-A frames shorter than 2 bytes.
Also guard the function entry: bail when `datalen < 1` before reading
`payload[0]` for the NAL type, so a zero-length frame does not read
past the payload.
`get_display_name_from_contact()` copied the SIP Contact display-name
into the caller's fixed 512-byte `remote_display_buf` with an unbounded
`strcpy`, so a stored dialog row with an over-long Contact could
overflow the stack buffer when a `dialog`-package SUBSCRIBE runs the
probe.
Thread the destination size into the helper and copy with
`switch_copy_string()`. Guard the helper against a zero
`dst_size` and a `NULL` or empty input, and always null-terminate the
destination.
In rtmp_rtp2rtmpH264 the STAP-A (type 24) aggregation loop read a
2-byte NALU length prefix at the end of the payload without ensuring
both bytes were in bounds, and copied each NALU without checking its
declared size against the remaining aggregation payload. Stop the loop
one byte earlier so the length prefix is always present, and skip any
NALU whose size exceeds the bytes left.
When building the AVC sequence header, validate the SPS and PPS sizes
before writing: require at least the 4 SPS profile bytes copied into
the header, and confirm the fixed framing plus both payloads fit `buf`
before emitting. Oversized SPS/PPS are logged and skipped instead of
overrunning the stack buffer.
`rtmp_rtmp2rtpH264()` parses two kinds of inbound H.264 RTMP video messages: an
AVC configuration record (`0x17/0x00`) that captures SPS/PPS, and NAL-unit
messages (`0x17`/`0x27` with `0x01`). Both paths trusted wire-supplied sizes and
counts without checking them against the bytes actually present, leading to
out-of-bounds reads.
NAL-unit walk:
- The length-prefix walk advanced the cursor and decremented the unsigned
remaining-byte counter by the wire NAL size with no check that the size fit.
A NAL size larger than the remaining payload underflowed the counter to near
`UINT32_MAX`, kept the loop running, and read the next size prefix from a
cursor already past the end of the buffer. The initializer
`pdata_len = len - 5` underflowed the same way for a message shorter than the
5-byte AVC header.
- Reject `len < 5`, change the loop guard to `pdata_len > lenSize` so each
size-prefix read stays in bounds, and reject any NAL whose declared size
exceeds the bytes remaining after its prefix.
AVC configuration record:
- The fixed header fields (`configurationVersion`, `lengthSizeMinusOne`,
`numOfSequenceParameterSets`) plus each 2-byte SPS/PPS length prefix and the
PPS count byte were read with no minimum-length check. The existing per-entry
checks bounded only the SPS/PPS body copies and ran after the length reads.
- Reject `len < 11` before the fixed header, and add a remaining-bytes check
before each `ntohs` length read and before the PPS count byte.
Both changes are correctness-only: well-formed records and NAL streams hit none
of the new guards. Malformed or truncated input is rejected with the existing
"corrupted data" diagnostic.
`rtmp_handle_control()` formats the control-message body into a fixed
200-byte stack buffer with an unbounded `sprintf` loop whose iteration
count is the wire message length. A body of ~70 bytes or more runs the
write off the end of `buf`, corrupting the stack frame; the length is
taken straight from the chunk header and reaches this path before any
login, so a remote peer can trigger it.
Bound the loop with `snprintf` against the remaining space and stop when
the buffer is full. This also caps the iteration count, so the loop can
no longer read `state->buf` past what was reassembled. The hex dump is
debug-only output, so capping it changes nothing operational.
In `msrp_parse_buffer()`, the `range_star` arm of `MSRP_ST_WAIT_BODY`
computed the body length by subtracting the delimiter length and trailing
framing from the received segment length. `payload_bytes` is a
`switch_size_t`, so a short segment wrapped it to near `SIZE_MAX`, which
`switch_msrp_msg_set_payload()` uses to size its allocation and as the
`memcpy` length. This affects both `len - dlen - 5`, whose scan pointer
also addressed memory before `buf`, and `delim_pos - buf - 2`, covered
only by a `switch_assert()` on received data.
Require room for the trailing end-line and the CRLF closing the body
before either is computed; a short segment is incomplete, so the parser
waits for more bytes.
`switch_core_media_add_crypto()` parses the SDP `a=crypto` keysalt and
decodes it into a fixed-size key buffer. Enforce the buffer contract at
the call site:
- Reject a zero-length, negative, or over-long keysalt (the length
check now has both a lower bound and an upper bound against the copy
buffer).
- Copy the keysalt token into a NUL-terminated buffer and decode from
that, so `switch_b64_decode` stops at the token instead of reading on
into the following key material (it consumes input to the NUL and
skips non-base64 bytes).
- Pass the destination buffer size as the decode bound rather than the
parsed token length.
- Require the decoded length to cover the crypto suite's key+salt so the
subsequent copy cannot read past the decoded bytes.
Add `test_add_crypto_keysalt_bounds`, a table-driven test covering the
accepted and rejected keysalt shapes across suites (AES-128/192/256),
including RFC 4568 lifetime/MKI and multi-key lines.
The RTCP NACK feedback handler used the packet's header length field as the
loop bound instead of the byte count the caller already validated. An
oversized length ran the loop past the fixed-size RTCP buffer, an
out-of-bounds access.
Clamp the entry count to the feedback words that fit in the received packet.
Add an end-to-end test feeding an oversized-length NACK through the RTCP
reader.
The 127-length extended payload field was decoded with `ntohl()`, a
32-bit byte swap, dropping the upper word of the 64-bit length. Combined
with the signed `issize_t plen`, a high low-word truncated to a negative
length that slipped past the signed size guard and became `SIZE_MAX` in
the read loop, driving an out-of-bounds write past `wsh->buffer`.
Decode the full 64-bit length in network byte order and reject any value
that cannot fit the buffer with an unsigned comparison before narrowing
to `issize_t`, so a truncated or oversized length can no longer yield a
negative `plen` or pass the guard.
`ws_write_frame()` had the symmetric defect: it encoded the 64-bit length
field with a 32-bit `htonl()`, mis-framing any payload large enough to
use the 8-byte (127) length field. Emit the full 8 bytes there too.
Factor the byte assembly into `ws_get_be64()` / `ws_put_be64()` helpers.
The nightmare-transfer path built its `switch_ivr_originate()` dial string by
interpolating the raw `Refer-To` URI params/headers into a
`{sip_invite_params=...}` brace block with `switch_core_sprintf()`.
`switch_ivr_originate()` splits a brace block on commas into separate channel
variables, so a comma in the remote-supplied URI params/headers was treated as a
variable separator and could set additional channel variables on the originated
leg.
Set `sip_invite_params` on the originate `ovars` event
(`nightmare_xfer_helper->vars`) instead. Values in that event are applied to the
peer channel verbatim and are never parsed for separators, so a comma stays
inside the single variable value. The composed value is unchanged
(`url_params?url_headers` when headers are present, `url_params` otherwise) and
`sofia_glue` reads `sip_invite_params` on the outbound leg as before, so a
well-formed REFER yields an identical outbound INVITE.
Drop the now-redundant `exten_with_params` helper field;
`nightmare_xfer_thread_run()` and the log use the plain
`nightmare_xfer_helper->exten` dial target directly.
Bound the XOR unmasking loop to `wsh->rplen` (payload only) instead of
`wsh->datalen`, which also covers the header and caused up to a 14-byte
OOB write past the frame payload in `wsh->buffer` using the
client-supplied mask key.
Tighten the size guard from `>` to `>=` to reserve 1 byte for the
trailing NUL written after the payload; a frame filling `buflen` exactly
otherwise NUL-wrote 1 byte past `wsh->buffer`.
Only reachable when `enable-websocket` is set in `mod_xml_rpc.conf.xml`
(off by default).
The positive-offset branch of `${variable:offset}` expansion advanced
`sub_val` past the cloned heap buffer without bounds-checking `offset`
against `strlen(sub_val)`. Subsequent `strlen()` and `strcat()` then
read adjacent heap memory into the output buffer.
* [Core, mod_commands] Interface allowlist (#3086)
* [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
Co-authored-by: Chris Rienzo <chris@signalwire.com>
* Merge commit from fork
Add `switch_stun_packet_verify_integrity()`, an HMAC-SHA1
MESSAGE-INTEGRITY verifier that is const and non-mutating: it runs
over a private copy of the pristine network-order packet, so the
caller's buffer and byte order stay untouched, and walks attributes
with its own unsigned bounded helper `stun_wire_attr_bounds()`
instead of the host-order iterator macros. A trailing
MESSAGE-INTEGRITY-SHA256 or FINGERPRINT after MESSAGE-INTEGRITY is
tolerated; any other trailing attribute is rejected.
Gate it in `handle_ice()` behind `ice->verify_integrity`: verify
before any ICE state is touched, keyed by message type (local
`ice->pass` for a request, remote `ice->rpass` for a response or
error response), and drop on failure. Keepalive indications carry no
MESSAGE-INTEGRITY and are ignored.
`ice->verify_integrity` is read from the `ice_verify_message_integrity`
channel variable in `switch_rtp_activate_ice()` and defaults off, so
receive-path behavior is unchanged unless it is enabled. Adds unit
tests in `tests/unit/switch_stun.c`.
* Merge commit from fork
* [core] Verify DTLS client cert against SDP fingerprint (server role)
Add opt-in verification of the client certificate when FreeSWITCH is
the DTLS server, mirroring the binding the client role already performs
on the server certificate: match the peer certificate against the SDP
`a=fingerprint` in `dtls_state_setup()`.
Selected per call by the `rtp_dtls_client_cert_verify_mode` channel
variable (`dtls_client_cert_verify_t`). An unset variable keeps the
default `DTLS_CLIENT_CERT_VERIFY_NONE`, so existing behavior is
unchanged:
- `none`: the server does not request a client certificate.
- `fingerprint`: request it (`SSL_VERIFY_PEER` + `dtls_accept_any_cert`)
and require its fingerprint to match the SDP value; a self-signed
certificate is accepted at the TLS layer and the match provides
authenticity.
- `full`: additionally let OpenSSL enforce the certificate chain.
An unrecognized mode string falls back to `fingerprint` (fail closed)
with a warning.
The mode is set per `SSL` object in `switch_rtp_add_dtls()`. Verification
fails (`DS_FAIL`, no SRTP keys derived) when the client presents no
certificate, its fingerprint does not match, or the peer advertised no
usable `a=fingerprint`: `get_evp_by_name()` returns `NULL` for a missing
or empty hash type and `switch_core_cert_extract_fingerprint()` rejects a
`NULL` algorithm rather than passing it to `X509_digest()`.
`conf/vanilla/vars.xml` documents the knob as a disabled example. Tests
in `tests/unit/switch_rtp.c` cover matching, mismatched, absent-cert,
absent-fingerprint, `none`, and unrecognized-mode cases.
---------
Co-authored-by: Andrey Volk <andywolk@gmail.com>
Co-authored-by: Chris Rienzo <chris@signalwire.com>
`read_packet()` passed a peer-supplied `Content-Length` straight to
`switch_zmalloc(body, clen + 1)`. Huge values drove `calloc` failure
and `switch_zmalloc` `abort()`-ed the daemon.
- Cap `Content-Length` at 16 MiB; reject negatives.
- Destroy the partially-built `*event` at the new rejection site and
at the existing body-recv failure path so callers don't leak it.
- Add `test_mod_event_socket` covering `INT_MAX`, above-cap,
negative, `atoi`-overflow, zero, and valid-non-zero-body cases.
Add `switch_stun_packet_verify_integrity()`, an HMAC-SHA1
MESSAGE-INTEGRITY verifier that is const and non-mutating: it runs
over a private copy of the pristine network-order packet, so the
caller's buffer and byte order stay untouched, and walks attributes
with its own unsigned bounded helper `stun_wire_attr_bounds()`
instead of the host-order iterator macros. A trailing
MESSAGE-INTEGRITY-SHA256 or FINGERPRINT after MESSAGE-INTEGRITY is
tolerated; any other trailing attribute is rejected.
Gate it in `handle_ice()` behind `ice->verify_integrity`: verify
before any ICE state is touched, keyed by message type (local
`ice->pass` for a request, remote `ice->rpass` for a response or
error response), and drop on failure. Keepalive indications carry no
MESSAGE-INTEGRITY and are ignored.
`ice->verify_integrity` is read from the `ice_verify_message_integrity`
channel variable in `switch_rtp_activate_ice()` and defaults off, so
receive-path behavior is unchanged unless it is enabled. Adds unit
tests in `tests/unit/switch_stun.c`.
A swig 4.1 regeneration dropped several hand-patched wrapper edits.
Express them (and the rest) as SWIG typemaps in `freeswitch.i` so they
survive reswig. `make reswig` now produces the final wrapper directly;
`hack.diff` and its `patch` step are removed.
Restored regressions:
- `setLUA(L)` on returned `Session`: a hangup hook or input callback on
a script-created `freeswitch.Session()` no longer crashes the process
- binary-safe `Stream::read` (`lua_pushlstring`)
Also moved to typemaps:
- `Dbh`/`JSON` self-pointer guards (`%typemap(check)`), now covering
every wrapper including four the hand-patch missed
- type-table isolation (`#define SWIG_TYPE_TABLE mod_lua`)
* [libesl] Validate `Content-Length` in `esl_recv_event`.
`atol()` accepted negative values, allowing a remote ESL peer to cause
a one-byte heap underwrite (`Content-Length: -1`) or NULL-pointer
dereference (`Content-Length: -2`, since `esl_assert` compiles out
under `NDEBUG`). Reject negative and oversized values, and check
`malloc` failure instead of relying on `assert`.
Cap at `ESL_MAX_CONTENT_LENGTH` (16 MiB).
* [libesl] Add test_recv_event.
Lower `CJSON_NESTING_LIMIT` from upstream default 1000 to 64 via
`SWITCH_AM_CFLAGS` / `SWITCH_AM_CXXFLAGS`. The mutually recursive
`parse_value`/`parse_array`/`parse_object` chain in cJSON consumes
~2 stack frames per nesting level, which can overflow worker
threads running on `SWITCH_THREAD_STACKSIZE` (240 KB).
In `check_auth()` the userauth branch committed request `userVariables`,
`JPFLAG_RESUME_CALL`, identity fields, `<user><params>`/`<variables>`,
`dialplan`, and `context` to `jsock` *before* the password compare. On
mismatch only `jsock->uid` was reverted; the rest persisted on the
socket and leaked into outbound/inbound INVITE setup and `jsapi`/event
publishes.
Restructure so the gate runs first: pre-scan `<user><params>` into
locals, compare, and on mismatch return FALSE with no `jsock` writes.
Identity/vars commits and `<user><params>`/`<variables>` persistence
move past the gate. Blind-reg short-circuit and
`req_params`/`x_user` ownership preserved on every exit; success-path
writes are bit-for-bit equivalent.
Side cleanups:
- "Login sucessful" → "Login successful" typo;
- success log WARNING → NOTICE;
- the spurious WARNING "Login sucessful" no longer fires on bad-password
attempts that located the user in the directory;
`process_jrpc()` called `set_session_id()` before `check_auth()`, so an
unauthenticated client could insert its jsock into `jsock_hash` under a
foreign `sessid` and have `attach_jsock()` evict the prior owner
(`verto.punt` + `detach_calls()` + `drop=1`) with no identity check.
Move the bind past the auth gate; `JPFLAG_INIT` now means "jsock is
bound", not "first frame seen". Additionally, `attach_jsock()` refuses
the bind when prior and new jsock are authed under different `uid`s,
replying `CODE_AUTH_FAILED` "Session in use". Same-uid reconnect and
no-auth profile binds are unchanged.
Unchecked `atoi()` on declared payload size let a client
request up to `INT_MAX`, forcing the server to write ~20 GB
per request via the download phase. Short `#` frames also
triggered OOB reads on `s[1..3]`.
- Gate `#` branch on `JPFLAG_AUTHED`.
- Cap declared size at 10 MiB (`VERTO_SPEED_TEST_MAX_SIZE`).
- Replace `atoi()` with bounded `strtol()`.
- Require `bytes >= 4` before indexing `s[1..3]`.
Cap `Content-Length` at `HTTP_POST_MAX_BODY` (10 MiB) and size the
allocation to the actual body length (`content_length + 1` for
the trailing NUL).
Also fix `WS_BLOCK` units — `kws_raw_read` takes ms, set to 10000.
Route IPv6 writes in `switch_stun_packet_attribute_add_binded_address`
and `switch_stun_packet_attribute_add_xor_binded_address` through
`switch_stun_ipv6_t` (16-byte `address[]`) instead of `switch_stun_ip_t`
(4-byte `uint32_t address`).
Add IPv4/IPv6 unit tests for both encoders.
Co-authored-by: Andrey Volk <andywolk@gmail.com>
Implement SIP 603+ (ATIS-1000099) support for FCC analytics-based call blocking compliance.
Detection:
- Detect incoming 603+ responses by checking "Network Blocked" phrase
and "v=analytics1;" in the `Reason` header text
- Set `sip_603plus_reason` channel variable on both legs for CDR visibility
Passthrough control:
- `sip_603plus_passthrough=true`: forward 603+ phrase and Reason header
- `sip_603plus_passthrough=false`: strip `Reason` header, send clean `603 Decline`
- Not set: existing behavior preserved
- Works independently of `disable_q850_reason` for selective forwarding
`sofia_process_dispatch_event_in_thread` allocated `td` from a memory pool,
then `sofia_msg_thread_run_once` destroyed that same pool after processing
the event — leaving `td` dangling when the thread pool worker accessed it.
Allocate `td` with `switch_zmalloc` (`td->alloc = 1`) so the worker frees it
safely after the function returns. Remove the now-unused `pool` field from
`sofia_dispatch_event_t`.
`switch_core_session_thread_pool_launch()` allocated the thread data (`td`)
from the session pool. However, `switch_core_session_thread()` destroys
the session pool before returning, leaving td as a dangling pointer.
The worker then accesses `td->running` and `td->pool` — a use-after-free
that crashes under memory pressure when the freed pool is reused.
Allocate `td` with `switch_zmalloc()` and set `td->alloc = 1` so the worker frees it
after the task completes. This ensures `td` outlives the session pool
destruction.
Changes:
- Snapshot `erl_errno` after `ei_xreceive_msg_tmo()` — outbound `ei_*` calls in the same loop iteration clobber the thread-local errno before the listener checks it, causing wrong exit decisions and misleading logs.
- Fix `switch_size_t ` cast of `int` in `ei_link`* — `(switch_size_t *)&index` reads/writes 8 bytes through a 4-byte `int` on LP64. Use a real `switch_size_t` local.
- Dispatch `ERL_NEWER_REFERENCE_EXT` — newer OTP encodes refs with this tag; spawn replies from modern nodes were silently dropped to the default branch.
- Handle `ERL_EXIT2` — processes killed via `erlang:exit/2` arrive with this tag, not `ERL_EXIT`. Without it, sessions stayed attached to dead Erlang pids.
- Modernize `-spec` syntax in `freeswitch.erl` — old `-spec(F/N :: (...))` form was removed in OTP 21+; module no longer compiled.
- Fix multiple memory issues:
- `ei_hash_ref()`: replace unbounded `sprintf` with `snprintf` + shared `EI_HASH_REF_LEN`.
- `handle_msg_sendevent` / `handle_msg_sendmsg`: free the heap `value` on `ei_decode_string` failure; remove dead `if (!fail)` branches.
- `listener_main_loop`: free `buf`/`rbuf` on the two `handle_msg` early-exit paths.
- `erlang_sendmsg_function` app: move `ei_x_new_with_version` past arg validation and add `ei_x_free` at the end.