[mod_silk] Bound decode loop to the codec's frame limit (#3116)
`switch_silk_decode()` ran its `do`/`while` decode loop while the SDK
kept reporting more internal frames, advancing the output pointer each
pass with no cap. The destination is a fixed-size PCM buffer, so a
stream reporting more internal frames than a conformant packet can
carry let the accumulated write run past its end.
Stop after `MAX_INPUT_FRAMES` internal frames, the most a conformant
SILK packet can hold, so the accumulated PCM stays within the
destination regardless of the bitstream.
Declare the per-pass sample count inside the loop so it resets to zero
each iteration, and advance the output pointer and length only when it
is positive. A pass that writes no samples, including a tolerated FEC
payload error that leaves the count untouched, then contributes nothing
instead of advancing on a stale count from an earlier pass or on a
negative value.
[core] Validate IPv6 XOR-MAPPED-ADDRESS length in STUN parser (#3115)
`switch_stun_packet_attribute_get_xor_mapped_address()` read and
XOR-rewrote a 16-byte IPv6 address whenever the `family` byte was 2,
without checking the attribute value was that long. Reject `family == 2`
values shorter than `sizeof(switch_stun_ipv6_t)` and clear the output
address and port. Adds a regression test.
[mod_xml_rpc] Fix OOB write and read-loop hang in WebSocket parser (#3114)
`ws_read_frame()` had two defects in the framing path:
- After header parsing, the remaining payload count
`need = plen - (datalen - header)` could go negative when the
initial read buffered more bytes than the frame's declared
length, even with an in-range `plen`. A negative `need` passed
the signed size guard and reached `ws_raw_read()` as a `size_t`
near its maximum, driving a `memcpy` past `wsh->buffer`. Reject
`need < 0` with a protocol-error close before the read loop.
- The loop filling the frame header called `ws_raw_read()` without
checking its result, so a connection that stopped delivering
header bytes left the loop with no terminating condition,
spinning or hanging the handler thread. Close on a non-advancing
read, matching the payload read loop.
[mod_rtmp] Harden H.264 video read path bounds and length parsing (#3113)
`rtmp_rtmp2rtpH264` read the two leading bytes that select the parse
branch (`data[0]` and `data[1]`) before any length check, so a video
body shorter than 2 bytes read past the buffer. Reject `len < 2` at
entry, before either classifier byte is dereferenced.
In the NAL-unit branch, move the `pdata = data + 5` assignment below
its `len < 5` check so the pointer is never formed past the end of a
short buffer.
Read the 2-byte big-endian SPS and PPS length prefixes byte-wise
(`(pdata[0] << 8) | pdata[1]`) instead of `ntohs(*(uint16_t *)pdata)`.
`pdata` walks a byte buffer at wire-controlled offsets, so the cast was
an unaligned 16-bit load and a strict-aliasing violation; the byte-wise
read is alignment- and endianness-independent and matches the idiom
already used in the NAL-unit branch.
In `rtmp_read_video_frame`, a buffered record is `len + 6` bytes (2-byte
length prefix, 4-byte timestamp, `len`-byte body), but the guard only
required `inuse >= len` before consuming the whole record. Require
`inuse >= len + 6` so the invariant holds locally instead of relying on
the writer emitting each record atomically.
[core] Harden STUN attribute parsing bounds and USERNAME copy (#3112)
Bounds and termination fixes across the STUN attribute receive path
in `handle_ice` and `switch_stun_lookup`:
- `switch_stun_packet_next_attribute` and its `_hbo` variant now
confirm the 4-byte attribute header is fully within `end` before
dereferencing `type`/`length`, and include the header when checking
that the value fits, so a truncated or overrunning attribute is not
read past the buffer.
- Compute `end_buf` as the 20-byte STUN header plus the attribute
section (`SWITCH_STUN_PACKET_MIN_LEN + header.length`) so the walk
covers every attribute, including trailing ones.
- Make `switch_stun_packet_next_attribute` the sole loop terminator
and drop the redundant `xlen` guard; its seed differed between the
two functions and could skip a trailing attribute in
`switch_stun_lookup`.
- `switch_stun_packet_attribute_get_username` reserves a byte for the
terminator and always NUL-terminates, since callers use the result
as a C string.
switch_b64_encode wrote each base64 character before checking the
output bound, and computed that bound as `bytes >= (int)olen - 1`.
Both are unsafe:
- Writing before the check meant a buffer with no room for a data
byte still received one: `olen == 1` wrote a character at index 0
and the NUL at index 1, and `olen == 0` (where `(int)olen - 1` is
-1) wrote two bytes, both past the end.
- Casting the unsigned `olen` to `int` truncated it above INT_MAX,
producing a wrong bound for very large buffers.
Reject `olen == 0`, make the output index `bytes` a `size_t`, and test
`bytes + 1 >= olen` before each write, so the bound never casts or
underflows and always leaves the last byte for the terminator. Apply
the same `bytes + 1 < olen` guard to the trailing partial-group
character and the `=` padding, so no write can pass the end.
Remove the dead line-wrap counter `y`: it only gated a commented-out
newline emit, so it counted and reset with no effect on the output.
Add unit tests covering the output-bound edges for every write site.
[core] Harden switch_b64_decode output bound and input handling (#3110)
switch_b64_decode bounded its writes with `ol >= olen - 1`, where
`olen` is unsigned. An `olen` of 0 made `olen - 1` wrap to `SIZE_MAX`,
so the bound never fired and the loop wrote the entire decoded input
plus a trailing NUL past the destination. Reject `olen == 0` and test
`ol + 1 >= olen` before each write, so the comparison never subtracts
from an unsigned and always leaves room for the terminator.
The alphabet lookup table `l64` was a `char` indexed by a `char`,
which is unsafe whichever way `char` is signed:
- Where `char` is signed, an input byte >= 0x80 became a negative
index and read before the table.
- Where `char` is unsigned, the `-1` "not in alphabet" sentinel was
stored as 255, so the skip test never matched and non-alphabet
bytes were folded in as data.
Make `l64` a `signed char` and index it with `(unsigned char)`, so the
sentinel survives and the index stays in range on every platform.
Change the bit accumulator `b` from `int` to `unsigned int` to avoid
signed-overflow undefined behavior on long input; decoded output is
unchanged.
Add unit tests: an encode/decode round-trip across the padding cases,
the output-bound edges (`olen` of 0, 1, and a truncating buffer), and
a non-alphabet byte (including one >= 0x80) that must be skipped.
Two invoke handlers read an AMF argument's value without first
checking its type, so a wrong-typed argument reads the wrong union
member:
- `rtmp_i_connect()` passed the first invoke argument straight into
`amf0_object_get()`, which walks the value as a node list. Treat
the command object as a field source only when it is an AMF object
or ECMA array, otherwise leave the lookups empty.
- `rtmp_i_receiveaudio()` and `rtmp_i_receivevideo()` read the flag
argument with `amf0_boolean_get_value()`, which returns the raw
union byte. Use `amf0_get_boolean()`, which yields `SWITCH_FALSE`
unless the argument is an AMF boolean.
In `udptl_rx_packet()`, decoded values from the error-recovery data were
used without checking them against their destination buffers:
- The FEC entry count is read as a single byte and drives both the
decode loop and the stored `fec_entries` later walked by the
reconstruction loop, allowing writes past the `fec[]`/`fec_len[]`
arrays, which hold only `LOCAL_FAX_MAX_FEC_PACKETS` entries. Reject a
larger count. `fec_entries` was also stored before the elements were
decoded, so a mid-loop reject left the slot advertising a count whose
`fec_len[]`/`fec[]` were stale or out of range; since `s->rx[]`
persists across packets, a later reconstruction pass could copy such a
stale length past the fixed `s->rx[].buf`. Commit the count only after
every element is decoded and bound-checked.
- Each redundancy secondary element length was copied into the fixed
`LOCAL_FAX_MAX_DATAGRAM`-sized `s->rx[].buf` without a size check,
unlike the primary packet. Reject an overlength element at decode
time.
`http_directory_auth()` builds the expected "user@domain:password"
token into `z[256]` and base64-encodes it to compare against the
client's Authorization header. Encode with `switch_b64_encode`, which
stops at its output-length argument and always NUL-terminates, and
size `t` from `sizeof(z)`: base64 emits 4 output bytes per 3 input
bytes (final partial group rounded up) plus a NUL, so
`4 * ((sizeof(z) + 2) / 3) + 1` holds the encoding of any `z`. The
encode is confined to `t`, matching the `switch_b64_decode` already
used for the inbound header.
Also in the same function:
- Drop the now-unused `#include <xmlrpc-c/base64_int.h>`, an xmlrpc-c
internal header that only declared the removed encoder.
- Compare 4 bytes, not 3, when stripping a leading `www.` from the
virtual-host `Host:` name. A 3-byte compare also matches hosts like
`www2.example.com` and then strips 4 bytes, yielding `.example.com`
and a failed directory lookup; for a bare `www` it advanced one
byte past the terminating NUL.
* [core] Fix XML escape encoder overrun and unsigned-char UTF-8 gate
`switch_xml_ampencode()` had two independent defects in its UTF-8
numeric-escape path.
Buffer overrun: the encoder grows its destination once per source byte,
but the realloc margin reserved only the 10 data characters of the
widest escape `"&#x%X;"` (a 21-bit code point rendered as 6 hex digits),
not the terminating NUL that `sprintf` also writes. At the margin
boundary that NUL landed one byte past the allocation. Reserve 11 bytes
in the guard (10 data chars plus the NUL) and emit the escape with
`snprintf` bounded to the remaining space, so the write stays in bounds
even if the margin is ever miscounted. The other escape sinks are all
within the widened margin and are unchanged.
Char signedness: the lead-byte test `(*s >> 8) & 0x01` reads bit 8 of a
plain `char`, which exists only after sign extension. Where `char` is
signed the high bit sign-extends and the test passes; where `char` is
unsigned it is always zero, so the numeric-escape path never ran and
multi-byte UTF-8 was emitted raw, making serialized XML differ by
architecture. Test bit 7 directly with `(*s & 0x80)`, correct regardless
of `char` signedness. This also makes the overrun fix effective on
unsigned-`char` builds, where the escape path now runs.
Add unit test `test_utf_8_wide_codepoint`, which serializes U+10FFFF and
long runs of it across buffer reallocations, sweeping an ASCII prefix so
an escape is emitted at the minimum-headroom offset, and asserts the
full serialized length.
`sofia_dialog_probe_callback()` filled its fixed 512-byte
`remote_display_buf` with unbounded `strcpy`. On the default path the
source is the stored To-header user part, which `sip_dialogs.sip_to_user`
holds verbatim from the wire with no truncation, so a dialog row
addressed to a user part longer than 511 bytes overflowed the buffer
when a `dialog`-package SUBSCRIBE ran the probe.
Copy with `snprintf()` bounded by `sizeof(remote_display_buf)` on every
path that writes it. The four proto branches write short fixed strings
and could not overflow; they are converted for consistency. User parts
under 512 bytes are copied unchanged.
[mod_rtmp] Guard FU-A and short frames in rtmp_rtp2rtmpH264 (#3109)
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] 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`.
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.
Jakub Karolczyk [Sat, 8 Aug 2026 14:10:31 +0000 (15:10 +0100)]
Merge commit from fork
* [core] Add protection for RTP inject DoS
* [core] Wipe malicious packet out
* [core] Introduce rtp_auto_adjustment_wait_for_advertised_ms chanvar to keep the auto-adjustment window opened for X ms or until packet from the source IP advertised in the SDP is received
* [core] Perform auto adjustment logic before bytes can be zeroed by flush
* [core] Add DDoS protection for auto-adjustment window with configurable threshold of packets per ptime from non-advertised source to be rejected. Should be carefully used in bursty environments. Disabled by default.
* [core] Track auto-adjust packets-per-ptime per source IP; wipe and yield CPU on rate-reject of flooding sources
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`.
Andrey Volk [Wed, 5 Aug 2026 15:44:33 +0000 (18:44 +0300)]
[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>
[mod_amqp] prevent segfault on double connection close
When a command response publish failed, `mod_amqp_command_response` closed
the connection and cleared conn_active. The command thread teardown then
called mod_amqp_connection_close(NULL), causing a segfault.
Also fix amqp_error_string2() calls to pass status codes without erroneous negation.
[mod_lua] Move SWIG wrapper patches into ".i" typemaps, drop hack.diff (#3043)
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`.
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]`.
[core] Use switch_stun_ipv6_t for STUN IPv6 write paths. (#3037)
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`).
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
[mod_sofia] Fix use-after-free in dispatch event thread. (#3031)
`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`.
[core] Fix use-after-free in session thread pool worker. (#3030)
`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.
[mod_erlang_event] Fix correctness, OTP compatibility, and memory issues
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.