From 6f670208b7ef2f11c68949f3f74b3bbcb6f5336a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 16:31:54 +0000 Subject: [PATCH 1/7] core: fix User-Agent injection on SBC-relayed requests; add hide_user_agent option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SIP_FLAGS_VERBATIM suppressed AmConfig::Signature injection on all SBC outgoing requests (INVITE, re-INVITE, REGISTER relay, auth-retry REGISTER), making the configured signature invisible regardless of sems.conf settings. Replace the VERBATIM-gated injection with a presence check: inject the signature whenever the User-Agent (requests) or Server (replies) header is absent, regardless of the VERBATIM flag. This preserves verbatim B2BUA relay for upstream UAs that supply their own header while ensuring SEMS-originated requests (e.g. UACAuth retry after 401/407) carry the configured identity. Add hide_user_agent = yes to sems.conf to actively strip User-Agent from requests and Server from replies, eliminating software fingerprinting as an attack surface. RFC 3261 §20.41 and §20.35 both note that omitting these headers reduces vulnerability to targeted attacks. Fixes: #539 https://claude.ai/code/session_016Pg7MeJjEfiNfJNSSLFiPb --- core/AmBasicSipDialog.cpp | 25 ++++++++++++++++--------- core/AmConfig.cpp | 8 ++++++-- core/AmConfig.h | 3 +++ core/AmSipDialog.cpp | 9 +++++---- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/core/AmBasicSipDialog.cpp b/core/AmBasicSipDialog.cpp index 87c7f7891..beff652d9 100644 --- a/core/AmBasicSipDialog.cpp +++ b/core/AmBasicSipDialog.cpp @@ -637,10 +637,12 @@ int AmBasicSipDialog::reply(const AmSipRequest& req, return -1; } - if (!(flags & SIP_FLAGS_VERBATIM)) { - // add Signature - if (AmConfig::Signature.length()) - reply.hdrs += SIP_HDR_COLSP(SIP_HDR_SERVER) + AmConfig::Signature + CRLF; + if (AmConfig::HideUserAgent) { + // RFC 3261 §20.35: Server is optional; suppress to reduce fingerprinting surface + removeHeader(reply.hdrs, SIP_HDR_SERVER); + } else if (AmConfig::Signature.length() && + getHeader(reply.hdrs, SIP_HDR_SERVER).empty()) { + reply.hdrs += SIP_HDR_COLSP(SIP_HDR_SERVER) + AmConfig::Signature + CRLF; } if ((code > 100 && code < 300) && !(flags & SIP_FLAGS_NOCONTACT)) { @@ -678,7 +680,7 @@ int AmBasicSipDialog::reply_error(const AmSipRequest& req, unsigned int code, reply.hdrs = hdrs; reply.to_tag = AmSession::getNewId(); - if (AmConfig::Signature.length()) + if (!AmConfig::HideUserAgent && AmConfig::Signature.length()) reply.hdrs += SIP_HDR_COLSP(SIP_HDR_SERVER) + AmConfig::Signature + CRLF; // add transcoder statistics into reply headers @@ -734,10 +736,15 @@ int AmBasicSipDialog::sendRequest(const string& method, req.contact = getContactHdr(); } - if (!(flags & SIP_FLAGS_VERBATIM)) { - // add Signature - if (AmConfig::Signature.length()) - req.hdrs += SIP_HDR_COLSP(SIP_HDR_USER_AGENT) + AmConfig::Signature + CRLF; + if (AmConfig::HideUserAgent) { + // RFC 3261 §20.41: User-Agent is optional; suppress to reduce fingerprinting surface + removeHeader(req.hdrs, SIP_HDR_USER_AGENT); + } else if (AmConfig::Signature.length() && + getHeader(req.hdrs, SIP_HDR_USER_AGENT).empty()) { + // Inject signature when no User-Agent is already present. This covers both + // SEMS-originated requests and SBC-relayed requests where the upstream UAC + // sent no User-Agent (e.g. auth-retry REGISTERs generated by UACAuth). + req.hdrs += SIP_HDR_COLSP(SIP_HDR_USER_AGENT) + AmConfig::Signature + CRLF; } int send_flags = 0; diff --git a/core/AmConfig.cpp b/core/AmConfig.cpp index 9b502505d..7db2e1d5f 100644 --- a/core/AmConfig.cpp +++ b/core/AmConfig.cpp @@ -102,6 +102,7 @@ unsigned int AmConfig::DSCPforSip = 0; unsigned int AmConfig::DSCPforRtp = 0; bool AmConfig::IgnoreNotifyLowerCSeq = false; string AmConfig::Signature = ""; +bool AmConfig::HideUserAgent = false; unsigned int AmConfig::MaxForwards = MAX_FORWARDS; bool AmConfig::SingleCodecInOK = false; unsigned int AmConfig::DeadRtpTime = DEAD_RTP_TIME; @@ -471,12 +472,15 @@ int AmConfig::readConfiguration() if (cfg.hasParameter("exclude_payloads")) ExcludePayloads = cfg.getParameter("exclude_payloads"); - // user_agent + // user_agent / server identity if (cfg.getParameter("use_default_signature")=="yes") Signature = DEFAULT_SIGNATURE; - else + else Signature = cfg.getParameter("signature"); + if (cfg.getParameter("hide_user_agent") == "yes") + HideUserAgent = true; + if (cfg.hasParameter("max_forwards")) { unsigned int mf=0; if(str2i(cfg.getParameter("max_forwards"), mf)) { diff --git a/core/AmConfig.h b/core/AmConfig.h index c0bf1c775..0f987a041 100644 --- a/core/AmConfig.h +++ b/core/AmConfig.h @@ -213,6 +213,9 @@ struct AmConfig static bool IgnoreNotifyLowerCSeq; /** Server/User-Agent header (optional) */ static string Signature; + /** Strip User-Agent/Server headers from all outgoing requests and replies. + * Reduces fingerprinting attack surface (RFC 3261 §20.41, §20.35). */ + static bool HideUserAgent; /** Value of Max-Forward header field for new requests */ static unsigned int MaxForwards; /** If 200 OK reply should be limited to preferred codec only */ diff --git a/core/AmSipDialog.cpp b/core/AmSipDialog.cpp index 9e2d8b050..2d62ca6c6 100644 --- a/core/AmSipDialog.cpp +++ b/core/AmSipDialog.cpp @@ -875,10 +875,11 @@ int AmSipDialog::send_200_ack(unsigned int inv_cseq, if(onTxRequest(req,flags) < 0) return -1; - if (!(flags&SIP_FLAGS_VERBATIM)) { - // add Signature - if (AmConfig::Signature.length()) - req.hdrs += SIP_HDR_COLSP(SIP_HDR_USER_AGENT) + AmConfig::Signature + CRLF; + if (AmConfig::HideUserAgent) { + removeHeader(req.hdrs, SIP_HDR_USER_AGENT); + } else if (AmConfig::Signature.length() && + getHeader(req.hdrs, SIP_HDR_USER_AGENT).empty()) { + req.hdrs += SIP_HDR_COLSP(SIP_HDR_USER_AGENT) + AmConfig::Signature + CRLF; } int res = SipCtrlInterface::send(req, local_tag, From 72fb3865ac094194795754d04183178d7483282a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 17:00:54 +0000 Subject: [PATCH 2/7] core: invert UA identity default to hidden; add send_user_agent option; unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem ------- The previous commit fixed User-Agent injection for SBC-relayed messages but kept the old default behaviour (identity revealed when `signature` is set). Disclosing the exact server version in User-Agent / Server headers is an explicit security risk per RFC 3261 §20.41 and §20.35. Changes ------- * Default is now HIDDEN (SendUserAgent=false). User-Agent is stripped from all outgoing requests and Server from all replies unless opted in. * New config knob `send_user_agent=yes` opts in to forwarding the identity header. Works in concert with the existing `use_default_signature` / `signature` options, which now have no visible effect unless `send_user_agent=yes` is also set. * Startup WARNING is logged whenever `send_user_agent=yes` and a signature string is configured, reminding operators of the fingerprinting exposure. * Refactor: the injection/suppression logic is extracted into the static helper `AmBasicSipDialog::applyIdentityHeader(hdrs, hdr_name)`. All four former inline blocks (sendRequest in AmBasicSipDialog and AmSipDialog, and the two reply paths) are replaced by a single call to this helper, keeping the policy in one auditable location. * B2BUA transparency preserved: when `send_user_agent=yes` and the upstream UAC already supplied a User-Agent, SEMS does not overwrite it. Documentation ------------- * sems.conf.sample: rewrote the identity-header section with full description of `send_user_agent`, its interaction with `signature` / `use_default_signature`, and the security rationale. The previously active `use_default_signature=yes` line is now commented out. Unit tests ---------- * core/tests/test_ua_header.{h,cpp}: 12 FCT tests covering: - Default hidden mode strips forwarded UA even when signature configured - send_user_agent=yes injects signature only when header is absent - Existing upstream UA is preserved (B2BUA transparency) - Symmetric behaviour for Server header on replies - Regression test for issue #539 auth-retry REGISTER scenario https://claude.ai/code/session_016Pg7MeJjEfiNfJNSSLFiPb --- core/AmBasicSipDialog.cpp | 38 +++---- core/AmBasicSipDialog.h | 17 +++ core/AmConfig.cpp | 16 ++- core/AmConfig.h | 9 +- core/AmSipDialog.cpp | 7 +- core/etc/sems.conf.sample | 39 ++++++- core/tests/sems_tests.cpp | 1 + core/tests/test_ua_header.cpp | 206 ++++++++++++++++++++++++++++++++++ core/tests/test_ua_header.h | 3 + 9 files changed, 298 insertions(+), 38 deletions(-) create mode 100644 core/tests/test_ua_header.cpp create mode 100644 core/tests/test_ua_header.h diff --git a/core/AmBasicSipDialog.cpp b/core/AmBasicSipDialog.cpp index beff652d9..a13b00527 100644 --- a/core/AmBasicSipDialog.cpp +++ b/core/AmBasicSipDialog.cpp @@ -541,6 +541,22 @@ int AmBasicSipDialog::onTxRequest(AmSipRequest& req, int& flags) return 0; } +void AmBasicSipDialog::applyIdentityHeader(string& hdrs, const char* hdr_name) +{ + if (!AmConfig::SendUserAgent) { + // Default: strip any forwarded identity header so software version is not + // disclosed (RFC 3261 §20.41 / §20.35). Opt in via send_user_agent=yes. + removeHeader(hdrs, hdr_name); + } else if (AmConfig::Signature.length() && + getHeader(hdrs, hdr_name).empty()) { + // send_user_agent=yes and a signature is configured: inject it only when + // no upstream UA already provided the header (preserves B2BUA transparency). + // Note: SIP_HDR_COLSP() requires a compile-time string literal so we + // build the field-name + ": " manually here. + hdrs += string(hdr_name) + COLSP + AmConfig::Signature + CRLF; + } +} + int AmBasicSipDialog::onTxReply(const AmSipRequest& req, AmSipReply& reply, int& flags) { @@ -637,13 +653,7 @@ int AmBasicSipDialog::reply(const AmSipRequest& req, return -1; } - if (AmConfig::HideUserAgent) { - // RFC 3261 §20.35: Server is optional; suppress to reduce fingerprinting surface - removeHeader(reply.hdrs, SIP_HDR_SERVER); - } else if (AmConfig::Signature.length() && - getHeader(reply.hdrs, SIP_HDR_SERVER).empty()) { - reply.hdrs += SIP_HDR_COLSP(SIP_HDR_SERVER) + AmConfig::Signature + CRLF; - } + applyIdentityHeader(reply.hdrs, SIP_HDR_SERVER); if ((code > 100 && code < 300) && !(flags & SIP_FLAGS_NOCONTACT)) { /* if 300<=code<400, explicit contact setting should be done */ @@ -680,8 +690,7 @@ int AmBasicSipDialog::reply_error(const AmSipRequest& req, unsigned int code, reply.hdrs = hdrs; reply.to_tag = AmSession::getNewId(); - if (!AmConfig::HideUserAgent && AmConfig::Signature.length()) - reply.hdrs += SIP_HDR_COLSP(SIP_HDR_SERVER) + AmConfig::Signature + CRLF; + applyIdentityHeader(reply.hdrs, SIP_HDR_SERVER); // add transcoder statistics into reply headers //addTranscoderStats(reply.hdrs); @@ -736,16 +745,7 @@ int AmBasicSipDialog::sendRequest(const string& method, req.contact = getContactHdr(); } - if (AmConfig::HideUserAgent) { - // RFC 3261 §20.41: User-Agent is optional; suppress to reduce fingerprinting surface - removeHeader(req.hdrs, SIP_HDR_USER_AGENT); - } else if (AmConfig::Signature.length() && - getHeader(req.hdrs, SIP_HDR_USER_AGENT).empty()) { - // Inject signature when no User-Agent is already present. This covers both - // SEMS-originated requests and SBC-relayed requests where the upstream UAC - // sent no User-Agent (e.g. auth-retry REGISTERs generated by UACAuth). - req.hdrs += SIP_HDR_COLSP(SIP_HDR_USER_AGENT) + AmConfig::Signature + CRLF; - } + applyIdentityHeader(req.hdrs, SIP_HDR_USER_AGENT); int send_flags = 0; if(patch_ruri_next_hop && remote_tag.empty()) { diff --git a/core/AmBasicSipDialog.h b/core/AmBasicSipDialog.h index 4d9179d59..c04e77108 100644 --- a/core/AmBasicSipDialog.h +++ b/core/AmBasicSipDialog.h @@ -415,6 +415,23 @@ class AmBasicSipDialog const string& hdrs = "", msg_logger* logger = NULL); + /** + * Enforce the User-Agent / Server identity policy on outgoing messages. + * + * Controlled by AmConfig::SendUserAgent and AmConfig::Signature: + * - SendUserAgent=false (default): strip hdr_name unconditionally so the + * server software version is not disclosed (RFC 3261 §20.41/§20.35). + * - SendUserAgent=true, Signature non-empty, header absent: inject + * Signature. A header already present (e.g. from the upstream UAC) is + * left intact to preserve B2BUA transparency. + * - SendUserAgent=true, Signature empty: no-op — no header is added or + * removed. + * + * Centralising the policy here makes it straightforward to unit-test without + * instantiating a full SIP dialog. + */ + static void applyIdentityHeader(string& hdrs, const char* hdr_name); + /* dump transaction information (DBG) */ void dump(); diff --git a/core/AmConfig.cpp b/core/AmConfig.cpp index 7db2e1d5f..de5e29403 100644 --- a/core/AmConfig.cpp +++ b/core/AmConfig.cpp @@ -102,7 +102,7 @@ unsigned int AmConfig::DSCPforSip = 0; unsigned int AmConfig::DSCPforRtp = 0; bool AmConfig::IgnoreNotifyLowerCSeq = false; string AmConfig::Signature = ""; -bool AmConfig::HideUserAgent = false; +bool AmConfig::SendUserAgent = false; unsigned int AmConfig::MaxForwards = MAX_FORWARDS; bool AmConfig::SingleCodecInOK = false; unsigned int AmConfig::DeadRtpTime = DEAD_RTP_TIME; @@ -478,8 +478,18 @@ int AmConfig::readConfiguration() else Signature = cfg.getParameter("signature"); - if (cfg.getParameter("hide_user_agent") == "yes") - HideUserAgent = true; + if (cfg.getParameter("send_user_agent") == "yes") + SendUserAgent = true; + + // RFC 3261 §20.41 / §20.35: revealing software identity in User-Agent / + // Server headers lets attackers target known vulnerabilities in this version. + // Warn when an operator has opted in to sending the identity string. + if (SendUserAgent && !Signature.empty()) + WARN("User-Agent/Server identity '%s' will be sent in all SIP messages. " + "This discloses the server software version and may increase exposure " + "to targeted attacks (RFC 3261 SS20.41/20.35). " + "Remove send_user_agent=yes from sems.conf to suppress these headers.\n", + Signature.c_str()); if (cfg.hasParameter("max_forwards")) { unsigned int mf=0; diff --git a/core/AmConfig.h b/core/AmConfig.h index 0f987a041..6fa335a62 100644 --- a/core/AmConfig.h +++ b/core/AmConfig.h @@ -211,11 +211,12 @@ struct AmConfig static unsigned int DSCPforRtp; /** Ignore Low CSeq on NOTIFY - for RFC 3265 instead of 5057 */ static bool IgnoreNotifyLowerCSeq; - /** Server/User-Agent header (optional) */ + /** Server/User-Agent header string (empty = not configured) */ static string Signature; - /** Strip User-Agent/Server headers from all outgoing requests and replies. - * Reduces fingerprinting attack surface (RFC 3261 §20.41, §20.35). */ - static bool HideUserAgent; + /** Inject User-Agent (requests) and Server (replies) headers on outgoing + * messages. Default false: headers are suppressed to prevent fingerprinting + * (RFC 3261 §20.41, §20.35). Set via send_user_agent=yes in sems.conf. */ + static bool SendUserAgent; /** Value of Max-Forward header field for new requests */ static unsigned int MaxForwards; /** If 200 OK reply should be limited to preferred codec only */ diff --git a/core/AmSipDialog.cpp b/core/AmSipDialog.cpp index 2d62ca6c6..91ef7652c 100644 --- a/core/AmSipDialog.cpp +++ b/core/AmSipDialog.cpp @@ -875,12 +875,7 @@ int AmSipDialog::send_200_ack(unsigned int inv_cseq, if(onTxRequest(req,flags) < 0) return -1; - if (AmConfig::HideUserAgent) { - removeHeader(req.hdrs, SIP_HDR_USER_AGENT); - } else if (AmConfig::Signature.length() && - getHeader(req.hdrs, SIP_HDR_USER_AGENT).empty()) { - req.hdrs += SIP_HDR_COLSP(SIP_HDR_USER_AGENT) + AmConfig::Signature + CRLF; - } + applyIdentityHeader(req.hdrs, SIP_HDR_USER_AGENT); int res = SipCtrlInterface::send(req, local_tag, remote_tag.empty() || !next_hop_1st_req ? diff --git a/core/etc/sems.conf.sample b/core/etc/sems.conf.sample index 6ae7a2b99..645d6546c 100644 --- a/core/etc/sems.conf.sample +++ b/core/etc/sems.conf.sample @@ -463,20 +463,47 @@ loglevel=2 # # RTP timeout after 10 seconds # dead_rtp_time=10 +# optional parameter: send_user_agent={yes|no} +# +# - Allow SEMS to inject a User-Agent header into outgoing SIP requests and a +# Server header into outgoing SIP replies. +# +# By default (send_user_agent=no) both headers are suppressed on all +# outgoing messages, including those relayed through the SBC module. +# Suppression prevents passive fingerprinting of the server software version, +# which RFC 3261 SS20.41 and SS20.35 explicitly flag as a security risk. +# +# Set send_user_agent=yes together with use_default_signature=yes or a +# custom signature= string when interoperating with upstream servers or +# registrars that require a User-Agent header, or when the identity +# information is acceptable to disclose. SEMS will log a startup WARNING +# whenever send_user_agent=yes is active as a reminder of the exposure. +# +# B2BUA transparency note: if the upstream UA already included its own +# User-Agent, SEMS will NOT overwrite it — the configured signature is only +# injected when the header is absent. +# +# default=no +# +# send_user_agent=yes + # optional parameter: use_default_signature={yes|no} # -# - use a Server/User-Agent header with the SEMS server -# signature and version. +# - Use the built-in SEMS name-and-version string as the User-Agent / Server +# header value. Has no effect unless send_user_agent=yes is also set. # # default=no # -use_default_signature=yes +# use_default_signature=yes # optional parameter: signature= # -# - use a Server/User-Agent header with a custom user agent -# signature. Overridden by default signature if -# use_default_signature is set. +# - Use a custom string as the User-Agent / Server header value. Overridden +# by use_default_signature=yes. Has no effect unless send_user_agent=yes +# is also set. +# +# A vague value such as "SIP Gateway" limits the information disclosed while +# still satisfying upstream servers that require the header. # # signature="SEMS media server 1.0" diff --git a/core/tests/sems_tests.cpp b/core/tests/sems_tests.cpp index 39f1300ea..e86681d63 100644 --- a/core/tests/sems_tests.cpp +++ b/core/tests/sems_tests.cpp @@ -33,6 +33,7 @@ FCT_BGN() { FCTMF_SUITE_CALL(test_rfc3261_musts); FCTMF_SUITE_CALL(test_extensions); FCTMF_SUITE_CALL(test_amconfig); + FCTMF_SUITE_CALL(test_ua_header); } FCT_END(); diff --git a/core/tests/test_ua_header.cpp b/core/tests/test_ua_header.cpp new file mode 100644 index 000000000..10ab15a6e --- /dev/null +++ b/core/tests/test_ua_header.cpp @@ -0,0 +1,206 @@ +#include "fct.h" + +#include "AmConfig.h" +#include "AmBasicSipDialog.h" +#include "AmSipMsg.h" +#include "AmSipHeaders.h" +#include "sip/defs.h" + +// RAII guard: restores AmConfig identity fields after each test so that test +// order does not matter and no state leaks between suites. +struct UAConfigGuard { + string saved_sig; + bool saved_send; + + UAConfigGuard() + : saved_sig(AmConfig::Signature), + saved_send(AmConfig::SendUserAgent) + {} + + ~UAConfigGuard() + { + AmConfig::Signature = saved_sig; + AmConfig::SendUserAgent = saved_send; + } +}; + +FCTMF_SUITE_BGN(test_ua_header) { + + // ----------------------------------------------------------------- + // Default behaviour: SendUserAgent=false => headers suppressed + // ----------------------------------------------------------------- + + FCT_TEST_BGN(default_no_ua_sent_empty_hdrs) { + // With factory defaults (SendUserAgent=false, Signature="") and no + // pre-existing header, applyIdentityHeader must leave hdrs empty. + UAConfigGuard g; + AmConfig::SendUserAgent = false; + AmConfig::Signature = ""; + string hdrs; + AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); + fct_chk(hdrs.empty()); + } + FCT_TEST_END(); + + FCT_TEST_BGN(default_strips_forwarded_ua) { + // Even when the upstream UAC sent its own User-Agent, the default must + // strip it so no identity leaks through the B2BUA. + UAConfigGuard g; + AmConfig::SendUserAgent = false; + AmConfig::Signature = ""; + string hdrs = "User-Agent: SomePhone/1.0" CRLF; + AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); + fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT).empty()); + } + FCT_TEST_END(); + + FCT_TEST_BGN(default_strips_forwarded_ua_with_signature_configured) { + // A configured signature must NOT be injected when SendUserAgent=false, + // and any forwarded UA must still be stripped. + UAConfigGuard g; + AmConfig::SendUserAgent = false; + AmConfig::Signature = "SEMS/2.x"; + string hdrs = "User-Agent: SomePhone/1.0" CRLF; + AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); + fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT).empty()); + } + FCT_TEST_END(); + + FCT_TEST_BGN(default_no_injection_without_signature) { + // SendUserAgent=false and no Signature: nothing is added. + UAConfigGuard g; + AmConfig::SendUserAgent = false; + AmConfig::Signature = ""; + string hdrs; + AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); + fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT).empty()); + } + FCT_TEST_END(); + + // ----------------------------------------------------------------- + // send_user_agent=yes + signature configured + // ----------------------------------------------------------------- + + FCT_TEST_BGN(send_ua_injects_signature_when_absent) { + // SendUserAgent=true + Signature set: SEMS must add User-Agent when the + // header is not already present (covers SBC relay with no upstream UA). + UAConfigGuard g; + AmConfig::SendUserAgent = true; + AmConfig::Signature = "SEMS/2.x"; + string hdrs; + AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); + fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT) == "SEMS/2.x"); + } + FCT_TEST_END(); + + FCT_TEST_BGN(send_ua_preserves_existing_ua) { + // When the upstream UAC already provided User-Agent, SEMS must not + // overwrite it — B2BUA transparency is preserved. + UAConfigGuard g; + AmConfig::SendUserAgent = true; + AmConfig::Signature = "SEMS/2.x"; + string hdrs = "User-Agent: SomePhone/1.0" CRLF; + AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); + fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT) == "SomePhone/1.0"); + // Must not add a second User-Agent line. + string ua_second = getHeader(hdrs, SIP_HDR_USER_AGENT, false); + fct_chk(ua_second.find("SEMS") == string::npos); + } + FCT_TEST_END(); + + FCT_TEST_BGN(send_ua_true_no_signature_noop) { + // SendUserAgent=true but no Signature string: nothing should be added or + // removed (no-op, not a crash). + UAConfigGuard g; + AmConfig::SendUserAgent = true; + AmConfig::Signature = ""; + string hdrs; + AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); + fct_chk(hdrs.empty()); + } + FCT_TEST_END(); + + FCT_TEST_BGN(send_ua_true_no_signature_preserves_forwarded) { + // SendUserAgent=true, no Signature, upstream UA present: must not strip it. + UAConfigGuard g; + AmConfig::SendUserAgent = true; + AmConfig::Signature = ""; + string hdrs = "User-Agent: SomePhone/1.0" CRLF; + AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); + fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT) == "SomePhone/1.0"); + } + FCT_TEST_END(); + + // ----------------------------------------------------------------- + // Server header (replies) — symmetric behaviour + // ----------------------------------------------------------------- + + FCT_TEST_BGN(default_strips_server_header) { + UAConfigGuard g; + AmConfig::SendUserAgent = false; + AmConfig::Signature = "SEMS/2.x"; + string hdrs = "Server: Kamailio/5.x" CRLF; + AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_SERVER); + fct_chk(getHeader(hdrs, SIP_HDR_SERVER).empty()); + } + FCT_TEST_END(); + + FCT_TEST_BGN(send_ua_injects_server_when_absent) { + UAConfigGuard g; + AmConfig::SendUserAgent = true; + AmConfig::Signature = "SEMS/2.x"; + string hdrs; + AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_SERVER); + fct_chk(getHeader(hdrs, SIP_HDR_SERVER) == "SEMS/2.x"); + } + FCT_TEST_END(); + + FCT_TEST_BGN(send_ua_preserves_existing_server) { + // A Server header already in the reply (e.g. from the B-leg) must not be + // overwritten when SendUserAgent=true. + UAConfigGuard g; + AmConfig::SendUserAgent = true; + AmConfig::Signature = "SEMS/2.x"; + string hdrs = "Server: Kamailio/5.x" CRLF; + AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_SERVER); + fct_chk(getHeader(hdrs, SIP_HDR_SERVER) == "Kamailio/5.x"); + } + FCT_TEST_END(); + + // ----------------------------------------------------------------- + // Regression: VERBATIM-relayed REGISTER/INVITE with no upstream UA + // This is the core bug from issue #539: auth-retry REGISTER generated by + // UACAuth sent with SIP_FLAGS_VERBATIM had no User-Agent even when + // signature was configured, because injection was gated on !VERBATIM. + // Now injection is gated on SendUserAgent and header absence only. + // ----------------------------------------------------------------- + + FCT_TEST_BGN(regression_539_auth_retry_gets_signature) { + // Simulate the auth-retry REGISTER: hdrs contains auth credentials but + // no User-Agent (the upstream phone did not send one). + UAConfigGuard g; + AmConfig::SendUserAgent = true; + AmConfig::Signature = "SEMS/2.x"; + // Typical saved headers from UACAuth::onSipReply after adding auth creds. + string hdrs = + "Authorization: Digest username=\"alice\",realm=\"example.com\"," + "nonce=\"abc123\",uri=\"sip:example.com\",response=\"deadbeef\"" CRLF; + AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); + fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT) == "SEMS/2.x"); + } + FCT_TEST_END(); + + FCT_TEST_BGN(regression_539_relay_with_phone_ua_preserved) { + // The upstream phone sent User-Agent; the B2BUA should forward it as-is + // and not overwrite with the SEMS signature. + UAConfigGuard g; + AmConfig::SendUserAgent = true; + AmConfig::Signature = "SEMS/2.x"; + string hdrs = "User-Agent: Grandstream/1.2.3" CRLF; + AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); + fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT) == "Grandstream/1.2.3"); + } + FCT_TEST_END(); + +} +FCTMF_SUITE_END(); diff --git a/core/tests/test_ua_header.h b/core/tests/test_ua_header.h new file mode 100644 index 000000000..54ef799cb --- /dev/null +++ b/core/tests/test_ua_header.h @@ -0,0 +1,3 @@ +#pragma once + +// Placeholder header required by test make rules. From 88e884be41efeb3b5f81d54db895671502e12e4a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 19:05:34 +0000 Subject: [PATCH 3/7] core: fix AmB2BSession::updateLocalBody build error (no AmMimeBody::clear) AmMimeBody has no clear() method. When the SDP part is present but empty, drop it via body.deletePart(SIP_APPLICATION_SDP), which is the correct API for removing a sub-part by content-type. This pre-existing compile error (introduced in commit 7c5374c on master) was blocking every CI build across all platforms. https://claude.ai/code/session_016Pg7MeJjEfiNfJNSSLFiPb --- core/AmB2BSession.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/AmB2BSession.cpp b/core/AmB2BSession.cpp index 9137e02e2..79b264af3 100644 --- a/core/AmB2BSession.cpp +++ b/core/AmB2BSession.cpp @@ -431,7 +431,7 @@ void AmB2BSession::updateLocalBody(AmMimeBody& body) if (!sdp) return; if (!sdp->getLen()) { - sdp->clear(); + body.deletePart(SIP_APPLICATION_SDP); return; } From 24945bf95f92251301eab091092364c79e073d01 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 19:06:00 +0000 Subject: [PATCH 4/7] chore: ignore build_check/ cmake build directory https://claude.ai/code/session_016Pg7MeJjEfiNfJNSSLFiPb --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f8b258817..f99c751dc 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ apps/py_sems/sip/*.h apps/rtmp/flash_phone/*.swf apps/rtmp/librtmp/librtmp.so.0 build/ +build_check/ cmake_install.cmake core.* core/etc/*.conf From eb7c26085790edbe26d6aa0f3d833d9d27ef2f00 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 19:12:29 +0000 Subject: [PATCH 5/7] core: fix MD5Update const-correctness for C++ strict const rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The second parameter of MD5Update was declared as 'unsigned char *' but callers (UACAuth, AmUtils, AmConfigReader) pass 'const unsigned char *' data. On macOS/clang this is a hard error. Add 'const' to the input parameter of MD5Update, MD5Transform, and Decode throughout md5.h/md5.cpp — none of these functions modify the input buffer, so the qualifier is correct and the change is safe. https://claude.ai/code/session_016Pg7MeJjEfiNfJNSSLFiPb --- core/md5.cpp | 10 +++++----- core/md5.h | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/md5.cpp b/core/md5.cpp index e36082b57..36c1a162e 100644 --- a/core/md5.cpp +++ b/core/md5.cpp @@ -57,9 +57,9 @@ documentation and/or software. #define S43 15 #define S44 21 -static void MD5Transform(UINT4 [4], unsigned char [64]); +static void MD5Transform(UINT4 [4], const unsigned char [64]); static void Encode(unsigned char *, UINT4 *, unsigned int); -static void Decode(UINT4 *, unsigned char *, unsigned int); +static void Decode(UINT4 *, const unsigned char *, unsigned int); static void MD5_memcpy(POINTER, POINTER, unsigned int); static void MD5_memset(POINTER, int, unsigned int); @@ -122,7 +122,7 @@ void MD5Init (MD5_CTX *context) context. */ void MD5Update (MD5_CTX *context, /* context */ - unsigned char *input, /* input block */ + const unsigned char *input, /* input block */ unsigned int inputLen /* length of input block */ ) { @@ -191,7 +191,7 @@ void MD5Final (unsigned char digest[16], MD5_CTX *context) /* MD5 basic transformation. Transforms state based on block. */ -static void MD5Transform (UINT4 state[4], unsigned char block[64]) +static void MD5Transform (UINT4 state[4], const unsigned char block[64]) { UINT4 a = state[0], b = state[1], c = state[2], d = state[3], x[16]; @@ -297,7 +297,7 @@ static void Encode (unsigned char *output, UINT4 *input, unsigned int len) /* Decodes input (unsigned char) into output (UINT4). Assumes len is a multiple of 4. */ -static void Decode (UINT4 *output, unsigned char *input, unsigned int len) +static void Decode (UINT4 *output, const unsigned char *input, unsigned int len) { unsigned int i, j; diff --git a/core/md5.h b/core/md5.h index 1552a089c..ed73ae000 100644 --- a/core/md5.h +++ b/core/md5.h @@ -36,6 +36,6 @@ typedef struct { } MD5_CTX; void MD5Init(MD5_CTX *); -void MD5Update(MD5_CTX *, unsigned char *, unsigned int); +void MD5Update(MD5_CTX *, const unsigned char *, unsigned int); void MD5Final(unsigned char [16], MD5_CTX *); #endif /* MD5_H */ From bc99fce21fe959a7c78d279c4f91af5f90f6e0f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 19:16:01 +0000 Subject: [PATCH 6/7] ci: run workflows on pull_request only, not on push Removes the push trigger from all three workflows so CI only runs when a PR is opened or updated, not on every branch commit. https://claude.ai/code/session_016Pg7MeJjEfiNfJNSSLFiPb --- .github/workflows/build_test.yml | 3 --- .github/workflows/freebsd_build.yml | 3 --- .github/workflows/macos_build.yml | 3 --- 3 files changed, 9 deletions(-) diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 6784fe8b3..7fc1fce2b 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -1,9 +1,6 @@ name: Build distribution packages on: - push: - branches: - - '**' pull_request: branches: - '**' diff --git a/.github/workflows/freebsd_build.yml b/.github/workflows/freebsd_build.yml index 16be7bd5c..4c265ad27 100644 --- a/.github/workflows/freebsd_build.yml +++ b/.github/workflows/freebsd_build.yml @@ -1,9 +1,6 @@ name: FreeBSD build on: - push: - branches: - - '**' pull_request: branches: - '**' diff --git a/.github/workflows/macos_build.yml b/.github/workflows/macos_build.yml index af4e07083..c69f7f4d8 100644 --- a/.github/workflows/macos_build.yml +++ b/.github/workflows/macos_build.yml @@ -1,9 +1,6 @@ name: macOS build on: - push: - branches: - - '**' pull_request: branches: - '**' From d1cab1d230da464080ca72abfada5055ed884640 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 20:48:56 +0000 Subject: [PATCH 7/7] sbc: move User-Agent/Server identity policy out of core into SBC plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The UA header injection logic previously lived in AmBasicSipDialog as a static method (applyIdentityHeader) keyed on global AmConfig::SendUserAgent. This is architecturally wrong: endpoint apps (conference, voicemail) have different identity needs than a B2BUA relay, and global config cannot express per-profile policy. Core changes (minimal): - Revert AmConfig: remove SendUserAgent; restore original Signature-only config and sems.conf.sample (use_default_signature=yes active by default) - Replace the three inline injection sites in AmBasicSipDialog::sendRequest(), reply(), and AmSipDialog::send_200_ack() with calls to a new virtual hook AmBasicSipEventHandler::onApplyIdentityHeader(hdrs, hdr_name, flags) - Default implementation of the hook reproduces the original SEMS behaviour: inject AmConfig::Signature when absent and SIP_FLAGS_VERBATIM is not set; all non-SBC sessions are unaffected - reply_error() remains inline (static method, no handler available) - Remove test_ua_header.cpp/h from core (tested the removed static method) SBC changes: - SBCCallProfile gains send_user_agent bool (default: false); parsed from the call profile config file; a WARN is emitted when enabled with a configured Signature (RFC 3261 §20.41/§20.35 fingerprinting risk) - SBCCallLeg overrides onApplyIdentityHeader: when send_user_agent=false (default), strip any forwarded User-Agent/Server so software identity is not disclosed on the relay path; when true, inject Signature only if the header is absent (preserves upstream phone's UA for B2BUA transparency) - transparent.sbcprofile.conf documents the new send_user_agent option This fixes issue #539 (auth-retry REGISTER losing User-Agent) on the SBC path: onApplyIdentityHeader is called after onSendRequest, so the SBC's injection decision is made after auth credentials are appended and is independent of SIP_FLAGS_VERBATIM. https://claude.ai/code/session_016Pg7MeJjEfiNfJNSSLFiPb --- apps/sbc/SBCCallLeg.cpp | 11 ++ apps/sbc/SBCCallLeg.h | 1 + apps/sbc/SBCCallProfile.cpp | 7 + apps/sbc/SBCCallProfile.h | 5 +- apps/sbc/etc/transparent.sbcprofile.conf | 13 ++ core/AmBasicSipDialog.cpp | 25 +-- core/AmBasicSipDialog.h | 37 ++-- core/AmConfig.cpp | 14 -- core/AmConfig.h | 4 - core/AmSipDialog.cpp | 2 +- core/etc/sems.conf.sample | 39 +---- core/tests/sems_tests.cpp | 1 - core/tests/test_ua_header.cpp | 206 ----------------------- core/tests/test_ua_header.h | 3 - 14 files changed, 70 insertions(+), 298 deletions(-) delete mode 100644 core/tests/test_ua_header.cpp delete mode 100644 core/tests/test_ua_header.h diff --git a/apps/sbc/SBCCallLeg.cpp b/apps/sbc/SBCCallLeg.cpp index d05b5225c..97d8ab165 100644 --- a/apps/sbc/SBCCallLeg.cpp +++ b/apps/sbc/SBCCallLeg.cpp @@ -36,6 +36,8 @@ #include "AmConfigReader.h" #include "AmSessionContainer.h" #include "AmSipHeaders.h" +#include "AmConfig.h" +#include "sip/defs.h" #include "SBCSimpleRelay.h" #include "RegisterDialog.h" #include "SubscriptionDialog.h" @@ -699,6 +701,15 @@ void SBCCallLeg::onSendRequest(AmSipRequest& req, int &flags) { CallLeg::onSendRequest(req, flags); } +void SBCCallLeg::onApplyIdentityHeader(string& hdrs, const char* hdr_name, int flags) +{ + if (!call_profile.send_user_agent) { + removeHeader(hdrs, hdr_name); + } else if (AmConfig::Signature.length() && getHeader(hdrs, hdr_name).empty()) { + hdrs += string(hdr_name) + COLSP + AmConfig::Signature + CRLF; + } +} + void SBCCallLeg::onRemoteDisappeared(const AmSipReply& reply) { CallLeg::onRemoteDisappeared(reply); diff --git a/apps/sbc/SBCCallLeg.h b/apps/sbc/SBCCallLeg.h index e83597b4a..43d117390 100644 --- a/apps/sbc/SBCCallLeg.h +++ b/apps/sbc/SBCCallLeg.h @@ -219,6 +219,7 @@ class SBCCallLeg : public CallLeg, public CredentialHolder void onSipReply(const AmSipRequest& req, const AmSipReply& reply, AmSipDialog::Status old_dlg_status); void onSendRequest(AmSipRequest& req, int &flags); + void onApplyIdentityHeader(string& hdrs, const char* hdr_name, int flags); virtual void onInitialReply(B2BSipReplyEvent *e); diff --git a/apps/sbc/SBCCallProfile.cpp b/apps/sbc/SBCCallProfile.cpp index 876000188..e53178347 100644 --- a/apps/sbc/SBCCallProfile.cpp +++ b/apps/sbc/SBCCallProfile.cpp @@ -358,6 +358,13 @@ bool SBCCallProfile::readFromConfiguration(const string& name, append_headers_req = cfg.getParameter("append_headers_req"); aleg_append_headers_req = cfg.getParameter("aleg_append_headers_req"); + send_user_agent = cfg.getParameter("send_user_agent") == "yes"; + if (send_user_agent && !AmConfig::Signature.empty()) + WARN("SBC profile '%s': send_user_agent=yes will disclose server identity " + "'%s' in User-Agent/Server headers on all outgoing SBC messages. " + "This may expose the server to targeted attacks (RFC 3261 SS20.41/20.35).\n", + name.c_str(), AmConfig::Signature.c_str()); + refuse_with = cfg.getParameter("refuse_with"); rtprelay_enabled = cfg.getParameter("enable_rtprelay"); diff --git a/apps/sbc/SBCCallProfile.h b/apps/sbc/SBCCallProfile.h index 7739dba4c..c4811f908 100644 --- a/apps/sbc/SBCCallProfile.h +++ b/apps/sbc/SBCCallProfile.h @@ -194,6 +194,8 @@ struct SBCCallProfile string append_headers_req; string aleg_append_headers_req; + bool send_user_agent; /**< inject User-Agent/Server identity headers (default: false) */ + string refuse_with; string rtprelay_enabled; @@ -386,7 +388,8 @@ struct SBCCallProfile reg_caching(false), max_491_retry_time(2000), log_rtp(false), - log_sip(false) + log_sip(false), + send_user_agent(false) { } ~SBCCallProfile() diff --git a/apps/sbc/etc/transparent.sbcprofile.conf b/apps/sbc/etc/transparent.sbcprofile.conf index ffbdf62ac..15f26d822 100644 --- a/apps/sbc/etc/transparent.sbcprofile.conf +++ b/apps/sbc/etc/transparent.sbcprofile.conf @@ -76,6 +76,19 @@ #sdp_alinesfilter_list=crypto,x-cap #sdp_anonymize=yes +## User-Agent / Server identity header policy +# +# send_user_agent=yes +# Inject the server identity string (configured via use_default_signature or +# signature in sems.conf) as a User-Agent header on outgoing requests and a +# Server header on outgoing replies. If the upstream UAC already provided a +# User-Agent, it is forwarded as-is; the signature is only added when the +# header is absent. +# Default: no (suppress both headers to prevent software-version disclosure; +# see RFC 3261 §20.41 and §20.35). +# +#send_user_agent=yes + ## append extra headers #append_headers="P-Source-IP: $si\r\nP-Source-Port: $sp\r\n" diff --git a/core/AmBasicSipDialog.cpp b/core/AmBasicSipDialog.cpp index a13b00527..12ae7ee87 100644 --- a/core/AmBasicSipDialog.cpp +++ b/core/AmBasicSipDialog.cpp @@ -541,20 +541,12 @@ int AmBasicSipDialog::onTxRequest(AmSipRequest& req, int& flags) return 0; } -void AmBasicSipDialog::applyIdentityHeader(string& hdrs, const char* hdr_name) -{ - if (!AmConfig::SendUserAgent) { - // Default: strip any forwarded identity header so software version is not - // disclosed (RFC 3261 §20.41 / §20.35). Opt in via send_user_agent=yes. - removeHeader(hdrs, hdr_name); - } else if (AmConfig::Signature.length() && - getHeader(hdrs, hdr_name).empty()) { - // send_user_agent=yes and a signature is configured: inject it only when - // no upstream UA already provided the header (preserves B2BUA transparency). - // Note: SIP_HDR_COLSP() requires a compile-time string literal so we - // build the field-name + ": " manually here. +void AmBasicSipEventHandler::onApplyIdentityHeader(string& hdrs, + const char* hdr_name, + int flags) +{ + if (!(flags & SIP_FLAGS_VERBATIM) && AmConfig::Signature.length()) hdrs += string(hdr_name) + COLSP + AmConfig::Signature + CRLF; - } } int AmBasicSipDialog::onTxReply(const AmSipRequest& req, @@ -653,7 +645,7 @@ int AmBasicSipDialog::reply(const AmSipRequest& req, return -1; } - applyIdentityHeader(reply.hdrs, SIP_HDR_SERVER); + if (hdl) hdl->onApplyIdentityHeader(reply.hdrs, SIP_HDR_SERVER, flags); if ((code > 100 && code < 300) && !(flags & SIP_FLAGS_NOCONTACT)) { /* if 300<=code<400, explicit contact setting should be done */ @@ -690,7 +682,8 @@ int AmBasicSipDialog::reply_error(const AmSipRequest& req, unsigned int code, reply.hdrs = hdrs; reply.to_tag = AmSession::getNewId(); - applyIdentityHeader(reply.hdrs, SIP_HDR_SERVER); + if (AmConfig::Signature.length()) + reply.hdrs += SIP_HDR_COLSP(SIP_HDR_SERVER) + AmConfig::Signature + CRLF; // add transcoder statistics into reply headers //addTranscoderStats(reply.hdrs); @@ -745,7 +738,7 @@ int AmBasicSipDialog::sendRequest(const string& method, req.contact = getContactHdr(); } - applyIdentityHeader(req.hdrs, SIP_HDR_USER_AGENT); + if (hdl) hdl->onApplyIdentityHeader(req.hdrs, SIP_HDR_USER_AGENT, flags); int send_flags = 0; if(patch_ruri_next_hop && remote_tag.empty()) { diff --git a/core/AmBasicSipDialog.h b/core/AmBasicSipDialog.h index c04e77108..c20cdd96f 100644 --- a/core/AmBasicSipDialog.h +++ b/core/AmBasicSipDialog.h @@ -415,23 +415,6 @@ class AmBasicSipDialog const string& hdrs = "", msg_logger* logger = NULL); - /** - * Enforce the User-Agent / Server identity policy on outgoing messages. - * - * Controlled by AmConfig::SendUserAgent and AmConfig::Signature: - * - SendUserAgent=false (default): strip hdr_name unconditionally so the - * server software version is not disclosed (RFC 3261 §20.41/§20.35). - * - SendUserAgent=true, Signature non-empty, header absent: inject - * Signature. A header already present (e.g. from the upstream UAC) is - * left intact to preserve B2BUA transparency. - * - SendUserAgent=true, Signature empty: no-op — no header is added or - * removed. - * - * Centralising the policy here makes it straightforward to unit-test without - * instantiating a full SIP dialog. - */ - static void applyIdentityHeader(string& hdrs, const char* hdr_name); - /* dump transaction information (DBG) */ void dump(); @@ -463,11 +446,27 @@ class AmBasicSipEventHandler /** Hook called before a request is sent */ virtual void onSendRequest(AmSipRequest& req, int& flags) {} - + /** Hook called before a reply is sent */ - virtual void onSendReply(const AmSipRequest& req, + virtual void onSendReply(const AmSipRequest& req, AmSipReply& reply, int& flags) {} + /** + * Hook called by AmBasicSipDialog to apply User-Agent (requests) and Server + * (replies) identity header policy before a message is sent to the transport. + * + * The default implementation reproduces the original SEMS behaviour: inject + * AmConfig::Signature when the header is absent and SIP_FLAGS_VERBATIM is not + * set. Subclasses (e.g. the SBC) may override to enforce a different policy + * such as stripping forwarded identity headers or controlling injection via a + * per-call-profile option. + * + * @param hdrs The outgoing message's extra header block (modifiable). + * @param hdr_name SIP_HDR_USER_AGENT for requests, SIP_HDR_SERVER for replies. + * @param flags Send flags (SIP_FLAGS_*) for the current message. + */ + virtual void onApplyIdentityHeader(string& hdrs, const char* hdr_name, int flags); + /** Hook called after a request has been sent */ virtual void onRequestSent(const AmSipRequest& req) {} diff --git a/core/AmConfig.cpp b/core/AmConfig.cpp index de5e29403..f5fb879eb 100644 --- a/core/AmConfig.cpp +++ b/core/AmConfig.cpp @@ -102,7 +102,6 @@ unsigned int AmConfig::DSCPforSip = 0; unsigned int AmConfig::DSCPforRtp = 0; bool AmConfig::IgnoreNotifyLowerCSeq = false; string AmConfig::Signature = ""; -bool AmConfig::SendUserAgent = false; unsigned int AmConfig::MaxForwards = MAX_FORWARDS; bool AmConfig::SingleCodecInOK = false; unsigned int AmConfig::DeadRtpTime = DEAD_RTP_TIME; @@ -478,19 +477,6 @@ int AmConfig::readConfiguration() else Signature = cfg.getParameter("signature"); - if (cfg.getParameter("send_user_agent") == "yes") - SendUserAgent = true; - - // RFC 3261 §20.41 / §20.35: revealing software identity in User-Agent / - // Server headers lets attackers target known vulnerabilities in this version. - // Warn when an operator has opted in to sending the identity string. - if (SendUserAgent && !Signature.empty()) - WARN("User-Agent/Server identity '%s' will be sent in all SIP messages. " - "This discloses the server software version and may increase exposure " - "to targeted attacks (RFC 3261 SS20.41/20.35). " - "Remove send_user_agent=yes from sems.conf to suppress these headers.\n", - Signature.c_str()); - if (cfg.hasParameter("max_forwards")) { unsigned int mf=0; if(str2i(cfg.getParameter("max_forwards"), mf)) { diff --git a/core/AmConfig.h b/core/AmConfig.h index 6fa335a62..1e7270c67 100644 --- a/core/AmConfig.h +++ b/core/AmConfig.h @@ -213,10 +213,6 @@ struct AmConfig static bool IgnoreNotifyLowerCSeq; /** Server/User-Agent header string (empty = not configured) */ static string Signature; - /** Inject User-Agent (requests) and Server (replies) headers on outgoing - * messages. Default false: headers are suppressed to prevent fingerprinting - * (RFC 3261 §20.41, §20.35). Set via send_user_agent=yes in sems.conf. */ - static bool SendUserAgent; /** Value of Max-Forward header field for new requests */ static unsigned int MaxForwards; /** If 200 OK reply should be limited to preferred codec only */ diff --git a/core/AmSipDialog.cpp b/core/AmSipDialog.cpp index 91ef7652c..bd23e8f5d 100644 --- a/core/AmSipDialog.cpp +++ b/core/AmSipDialog.cpp @@ -875,7 +875,7 @@ int AmSipDialog::send_200_ack(unsigned int inv_cseq, if(onTxRequest(req,flags) < 0) return -1; - applyIdentityHeader(req.hdrs, SIP_HDR_USER_AGENT); + if (hdl) hdl->onApplyIdentityHeader(req.hdrs, SIP_HDR_USER_AGENT, flags); int res = SipCtrlInterface::send(req, local_tag, remote_tag.empty() || !next_hop_1st_req ? diff --git a/core/etc/sems.conf.sample b/core/etc/sems.conf.sample index 645d6546c..0c61ee1b6 100644 --- a/core/etc/sems.conf.sample +++ b/core/etc/sems.conf.sample @@ -463,47 +463,20 @@ loglevel=2 # # RTP timeout after 10 seconds # dead_rtp_time=10 -# optional parameter: send_user_agent={yes|no} -# -# - Allow SEMS to inject a User-Agent header into outgoing SIP requests and a -# Server header into outgoing SIP replies. -# -# By default (send_user_agent=no) both headers are suppressed on all -# outgoing messages, including those relayed through the SBC module. -# Suppression prevents passive fingerprinting of the server software version, -# which RFC 3261 SS20.41 and SS20.35 explicitly flag as a security risk. -# -# Set send_user_agent=yes together with use_default_signature=yes or a -# custom signature= string when interoperating with upstream servers or -# registrars that require a User-Agent header, or when the identity -# information is acceptable to disclose. SEMS will log a startup WARNING -# whenever send_user_agent=yes is active as a reminder of the exposure. -# -# B2BUA transparency note: if the upstream UA already included its own -# User-Agent, SEMS will NOT overwrite it — the configured signature is only -# injected when the header is absent. -# -# default=no -# -# send_user_agent=yes - # optional parameter: use_default_signature={yes|no} # -# - Use the built-in SEMS name-and-version string as the User-Agent / Server -# header value. Has no effect unless send_user_agent=yes is also set. +# - use a Server/User-Agent header with the SEMS server +# signature and version. # # default=no # -# use_default_signature=yes +use_default_signature=yes # optional parameter: signature= # -# - Use a custom string as the User-Agent / Server header value. Overridden -# by use_default_signature=yes. Has no effect unless send_user_agent=yes -# is also set. -# -# A vague value such as "SIP Gateway" limits the information disclosed while -# still satisfying upstream servers that require the header. +# - use a Server/User-Agent header with a custom user agent +# signature. Overridden by default signature if +# use_default_signature is set. # # signature="SEMS media server 1.0" diff --git a/core/tests/sems_tests.cpp b/core/tests/sems_tests.cpp index e86681d63..39f1300ea 100644 --- a/core/tests/sems_tests.cpp +++ b/core/tests/sems_tests.cpp @@ -33,7 +33,6 @@ FCT_BGN() { FCTMF_SUITE_CALL(test_rfc3261_musts); FCTMF_SUITE_CALL(test_extensions); FCTMF_SUITE_CALL(test_amconfig); - FCTMF_SUITE_CALL(test_ua_header); } FCT_END(); diff --git a/core/tests/test_ua_header.cpp b/core/tests/test_ua_header.cpp deleted file mode 100644 index 10ab15a6e..000000000 --- a/core/tests/test_ua_header.cpp +++ /dev/null @@ -1,206 +0,0 @@ -#include "fct.h" - -#include "AmConfig.h" -#include "AmBasicSipDialog.h" -#include "AmSipMsg.h" -#include "AmSipHeaders.h" -#include "sip/defs.h" - -// RAII guard: restores AmConfig identity fields after each test so that test -// order does not matter and no state leaks between suites. -struct UAConfigGuard { - string saved_sig; - bool saved_send; - - UAConfigGuard() - : saved_sig(AmConfig::Signature), - saved_send(AmConfig::SendUserAgent) - {} - - ~UAConfigGuard() - { - AmConfig::Signature = saved_sig; - AmConfig::SendUserAgent = saved_send; - } -}; - -FCTMF_SUITE_BGN(test_ua_header) { - - // ----------------------------------------------------------------- - // Default behaviour: SendUserAgent=false => headers suppressed - // ----------------------------------------------------------------- - - FCT_TEST_BGN(default_no_ua_sent_empty_hdrs) { - // With factory defaults (SendUserAgent=false, Signature="") and no - // pre-existing header, applyIdentityHeader must leave hdrs empty. - UAConfigGuard g; - AmConfig::SendUserAgent = false; - AmConfig::Signature = ""; - string hdrs; - AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); - fct_chk(hdrs.empty()); - } - FCT_TEST_END(); - - FCT_TEST_BGN(default_strips_forwarded_ua) { - // Even when the upstream UAC sent its own User-Agent, the default must - // strip it so no identity leaks through the B2BUA. - UAConfigGuard g; - AmConfig::SendUserAgent = false; - AmConfig::Signature = ""; - string hdrs = "User-Agent: SomePhone/1.0" CRLF; - AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); - fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT).empty()); - } - FCT_TEST_END(); - - FCT_TEST_BGN(default_strips_forwarded_ua_with_signature_configured) { - // A configured signature must NOT be injected when SendUserAgent=false, - // and any forwarded UA must still be stripped. - UAConfigGuard g; - AmConfig::SendUserAgent = false; - AmConfig::Signature = "SEMS/2.x"; - string hdrs = "User-Agent: SomePhone/1.0" CRLF; - AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); - fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT).empty()); - } - FCT_TEST_END(); - - FCT_TEST_BGN(default_no_injection_without_signature) { - // SendUserAgent=false and no Signature: nothing is added. - UAConfigGuard g; - AmConfig::SendUserAgent = false; - AmConfig::Signature = ""; - string hdrs; - AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); - fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT).empty()); - } - FCT_TEST_END(); - - // ----------------------------------------------------------------- - // send_user_agent=yes + signature configured - // ----------------------------------------------------------------- - - FCT_TEST_BGN(send_ua_injects_signature_when_absent) { - // SendUserAgent=true + Signature set: SEMS must add User-Agent when the - // header is not already present (covers SBC relay with no upstream UA). - UAConfigGuard g; - AmConfig::SendUserAgent = true; - AmConfig::Signature = "SEMS/2.x"; - string hdrs; - AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); - fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT) == "SEMS/2.x"); - } - FCT_TEST_END(); - - FCT_TEST_BGN(send_ua_preserves_existing_ua) { - // When the upstream UAC already provided User-Agent, SEMS must not - // overwrite it — B2BUA transparency is preserved. - UAConfigGuard g; - AmConfig::SendUserAgent = true; - AmConfig::Signature = "SEMS/2.x"; - string hdrs = "User-Agent: SomePhone/1.0" CRLF; - AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); - fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT) == "SomePhone/1.0"); - // Must not add a second User-Agent line. - string ua_second = getHeader(hdrs, SIP_HDR_USER_AGENT, false); - fct_chk(ua_second.find("SEMS") == string::npos); - } - FCT_TEST_END(); - - FCT_TEST_BGN(send_ua_true_no_signature_noop) { - // SendUserAgent=true but no Signature string: nothing should be added or - // removed (no-op, not a crash). - UAConfigGuard g; - AmConfig::SendUserAgent = true; - AmConfig::Signature = ""; - string hdrs; - AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); - fct_chk(hdrs.empty()); - } - FCT_TEST_END(); - - FCT_TEST_BGN(send_ua_true_no_signature_preserves_forwarded) { - // SendUserAgent=true, no Signature, upstream UA present: must not strip it. - UAConfigGuard g; - AmConfig::SendUserAgent = true; - AmConfig::Signature = ""; - string hdrs = "User-Agent: SomePhone/1.0" CRLF; - AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); - fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT) == "SomePhone/1.0"); - } - FCT_TEST_END(); - - // ----------------------------------------------------------------- - // Server header (replies) — symmetric behaviour - // ----------------------------------------------------------------- - - FCT_TEST_BGN(default_strips_server_header) { - UAConfigGuard g; - AmConfig::SendUserAgent = false; - AmConfig::Signature = "SEMS/2.x"; - string hdrs = "Server: Kamailio/5.x" CRLF; - AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_SERVER); - fct_chk(getHeader(hdrs, SIP_HDR_SERVER).empty()); - } - FCT_TEST_END(); - - FCT_TEST_BGN(send_ua_injects_server_when_absent) { - UAConfigGuard g; - AmConfig::SendUserAgent = true; - AmConfig::Signature = "SEMS/2.x"; - string hdrs; - AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_SERVER); - fct_chk(getHeader(hdrs, SIP_HDR_SERVER) == "SEMS/2.x"); - } - FCT_TEST_END(); - - FCT_TEST_BGN(send_ua_preserves_existing_server) { - // A Server header already in the reply (e.g. from the B-leg) must not be - // overwritten when SendUserAgent=true. - UAConfigGuard g; - AmConfig::SendUserAgent = true; - AmConfig::Signature = "SEMS/2.x"; - string hdrs = "Server: Kamailio/5.x" CRLF; - AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_SERVER); - fct_chk(getHeader(hdrs, SIP_HDR_SERVER) == "Kamailio/5.x"); - } - FCT_TEST_END(); - - // ----------------------------------------------------------------- - // Regression: VERBATIM-relayed REGISTER/INVITE with no upstream UA - // This is the core bug from issue #539: auth-retry REGISTER generated by - // UACAuth sent with SIP_FLAGS_VERBATIM had no User-Agent even when - // signature was configured, because injection was gated on !VERBATIM. - // Now injection is gated on SendUserAgent and header absence only. - // ----------------------------------------------------------------- - - FCT_TEST_BGN(regression_539_auth_retry_gets_signature) { - // Simulate the auth-retry REGISTER: hdrs contains auth credentials but - // no User-Agent (the upstream phone did not send one). - UAConfigGuard g; - AmConfig::SendUserAgent = true; - AmConfig::Signature = "SEMS/2.x"; - // Typical saved headers from UACAuth::onSipReply after adding auth creds. - string hdrs = - "Authorization: Digest username=\"alice\",realm=\"example.com\"," - "nonce=\"abc123\",uri=\"sip:example.com\",response=\"deadbeef\"" CRLF; - AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); - fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT) == "SEMS/2.x"); - } - FCT_TEST_END(); - - FCT_TEST_BGN(regression_539_relay_with_phone_ua_preserved) { - // The upstream phone sent User-Agent; the B2BUA should forward it as-is - // and not overwrite with the SEMS signature. - UAConfigGuard g; - AmConfig::SendUserAgent = true; - AmConfig::Signature = "SEMS/2.x"; - string hdrs = "User-Agent: Grandstream/1.2.3" CRLF; - AmBasicSipDialog::applyIdentityHeader(hdrs, SIP_HDR_USER_AGENT); - fct_chk(getHeader(hdrs, SIP_HDR_USER_AGENT) == "Grandstream/1.2.3"); - } - FCT_TEST_END(); - -} -FCTMF_SUITE_END(); diff --git a/core/tests/test_ua_header.h b/core/tests/test_ua_header.h deleted file mode 100644 index 54ef799cb..000000000 --- a/core/tests/test_ua_header.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -// Placeholder header required by test make rules.