-
Notifications
You must be signed in to change notification settings - Fork 13.6k
feat: Contact name and avatar resolution for external voice calls #40484
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| --- | ||
| '@rocket.chat/media-calls': minor | ||
| '@rocket.chat/model-typings': minor | ||
| '@rocket.chat/ui-voip': minor | ||
| '@rocket.chat/models': minor | ||
| '@rocket.chat/i18n': minor | ||
| '@rocket.chat/core-typings': minor | ||
| '@rocket.chat/rest-typings': minor | ||
| '@rocket.chat/meteor': minor | ||
| --- | ||
|
|
||
| Adds name and avatar resolution for external voice calls |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -439,39 +439,44 @@ export class IncomingSipCall extends BaseSipCall { | |
| throw new SipError(SipErrorCodes.NOT_FOUND); | ||
| } | ||
|
|
||
| private static async getRocketChatCallerFromInvite(req: SrfRequest): Promise<MediaCallContact | null> { | ||
| logger.debug({ | ||
| msg: 'IncomingSipCall.getRocketChatCallerFromInvite', | ||
| callingNumber: req.callingNumber, | ||
| calledNumber: req.calledNumber, | ||
| }); | ||
| private static getDisplayNameFromInvite(req: SrfRequest): string | undefined { | ||
| const removeQuotes = (str?: string): string | undefined => str?.replace(/^"|"$/g, '').trim(); | ||
|
|
||
| if (req.callingNumber && typeof req.callingNumber === 'string') { | ||
| const userContact = await mediaCallDirector.cast.getContactForExtensionNumber(req.callingNumber, { preferredType: 'sip' }); | ||
| if (userContact) { | ||
| return userContact; | ||
| if (req.has('X-RocketChat-Caller-Name')) { | ||
| const headerValue = req.get('X-RocketChat-Caller-Name'); | ||
| if (headerValue) { | ||
| return headerValue; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| if (req.has('p-asserted-identity')) { | ||
| const pAssertedIdentity = removeQuotes(req.getParsedHeader('p-asserted-identity')?.name); | ||
| if (pAssertedIdentity) { | ||
| return pAssertedIdentity; | ||
| } | ||
| } | ||
|
|
||
| if (req.has('from')) { | ||
| const fromHeader = removeQuotes(req.getParsedHeader('from')?.name); | ||
| if (fromHeader) { | ||
| return fromHeader; | ||
| } | ||
| } | ||
|
|
||
| return undefined; | ||
| } | ||
|
|
||
| private static async getCallerContactFromInvite(sessionId: string, req: SrfRequest): Promise<MediaCallSignedContact<'sip'>> { | ||
| logger.debug({ msg: 'IncomingSipCall.getCallerContactFromInvite' }); | ||
| const callerBase = await this.getRocketChatCallerFromInvite(req); | ||
|
|
||
| const displayNameFromHeader = req.has('X-RocketChat-Caller-Name') && req.get('X-RocketChat-Caller-Name'); | ||
| const displayName = this.getDisplayNameFromInvite(req); | ||
| const usernameFromHeader = req.has('X-RocketChat-Caller-Username') && req.get('X-RocketChat-Caller-Username'); | ||
|
|
||
| const displayName = displayNameFromHeader || callerBase?.displayName || req.from; | ||
| const username = usernameFromHeader || callerBase?.username || req.callingNumber; | ||
|
|
||
| const sipExtension = req.callingNumber; | ||
|
|
||
| const defaultContactInfo: MediaCallContactInformation = { | ||
| username, | ||
| sipExtension, | ||
| displayName: displayName || sipExtension, | ||
| ...(usernameFromHeader && { username: usernameFromHeader }), | ||
|
Comment on lines
+472
to
+479
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium: SIP Caller Identity Spoofing via Unverified Headers The IncomingSipCall.getCallerContactFromInvite method trusts SIP headers (X-RocketChat-Caller-Name, X-RocketChat-Caller-Username, p-asserted-identity, from) to construct the caller's identity (MediaCallContactInformation). These headers are provided by the external SIP peer and are not cryptographically verified. An attacker can spoof the caller's display name and username, which are then used throughout the application to represent the caller, potentially leading to social engineering or impersonation attacks within the Rocket.Chat interface. Steps to Reproduce
# Not applicable for SIP. Use the bash script PoC.# This is a conceptual PoC.
# An attacker sends a SIP INVITE with spoofed headers.
# The server will process these headers and display the spoofed identity to the callee.
cat <<EOF | nc <SIP_SERVER_IP> 5060
INVITE sip:target@example.com SIP/2.0
Via: SIP/2.0/UDP attacker.com;branch=z9hG4bK123
From: "Spoofed User" <sip:attacker@attacker.com>;tag=123
To: <sip:target@example.com>
Call-ID: 123@attacker.com
CSeq: 1 INVITE
X-RocketChat-Caller-Name: "CEO"
X-RocketChat-Caller-Username: "admin"
Content-Type: application/sdp
Content-Length: 0
EOF# Not applicable for SIP. Use the bash script PoC.PoC Url: N/A Fix with AITriage: Reply
pierre-lehnen-rc marked this conversation as resolved.
|
||
| }; | ||
|
|
||
| const contact = await mediaCallDirector.cast.getContactForExtensionNumber(sipExtension, { requiredType: 'sip' }, defaultContactInfo); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import InternalUser from './InternalUser'; | ||
| import PhoneNumber from './PhoneNumber'; | ||
|
|
||
| export type ExternalUserProps = { | ||
| number: string; | ||
| displayName?: string; | ||
| avatarUrl?: string; | ||
| }; | ||
|
|
||
| const ExternalUser = ({ number, displayName, avatarUrl }: ExternalUserProps) => { | ||
| if (displayName) { | ||
| return <InternalUser displayName={displayName} avatarUrl={avatarUrl} callerId={number} />; | ||
| } | ||
|
|
||
| return <PhoneNumber number={number} />; | ||
|
pierre-lehnen-rc marked this conversation as resolved.
|
||
| }; | ||
|
|
||
| export default ExternalUser; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,15 @@ | ||
| import type { ComponentProps } from 'react'; | ||
| import { ExternalUser, InternalUser } from '.'; | ||
| import type { ExternalUserProps } from './ExternalUser'; | ||
| import type { InternalUserProps } from './InternalUser'; | ||
|
|
||
| import { InternalUser, PhoneNumber } from '.'; | ||
|
|
||
| export type PeerInfoProps = ComponentProps<typeof InternalUser> | ComponentProps<typeof PhoneNumber>; | ||
| export type PeerInfoProps = (InternalUserProps & { external?: false }) | (ExternalUserProps & { external: true }); | ||
|
|
||
| const PeerInfo = (props: PeerInfoProps) => { | ||
| if ('displayName' in props) { | ||
| return <InternalUser {...props} />; | ||
| if (props.external) { | ||
| return <ExternalUser {...props} />; | ||
| } | ||
| return <PhoneNumber {...props} />; | ||
|
|
||
| return <InternalUser {...props} />; | ||
| }; | ||
|
|
||
| export default PeerInfo; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| export { default as InternalUser } from './InternalUser'; | ||
| export { default as ExternalUser } from './ExternalUser'; | ||
| export { default as PhoneNumber } from './PhoneNumber'; | ||
| export { default as PeerInfo } from './PeerInfo'; | ||
| export { type PeerInfoProps } from './PeerInfo'; |
Uh oh!
There was an error while loading. Please reload this page.