Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c361a21d68 | ||
|
|
f49694f8c9 |
+1
-1
@@ -1 +1 @@
|
|||||||
4.14.1
|
4.14.0
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts
|
|||||||
{
|
{
|
||||||
redirect_uri: "#{base_url}/microsoft/callback",
|
redirect_uri: "#{base_url}/microsoft/callback",
|
||||||
scope: scope,
|
scope: scope,
|
||||||
state: state
|
state: state,
|
||||||
|
prompt: 'consent'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if redirect_url
|
if redirect_url
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
|||||||
def process_hmac
|
def process_hmac
|
||||||
return if params[:identifier_hash].blank? && !@inbox_channel.hmac_mandatory
|
return if params[:identifier_hash].blank? && !@inbox_channel.hmac_mandatory
|
||||||
raise StandardError, 'HMAC failed: Invalid Identifier Hash Provided' unless valid_hmac?
|
raise StandardError, 'HMAC failed: Invalid Identifier Hash Provided' unless valid_hmac?
|
||||||
|
raise StandardError, 'HMAC failed: Identifier does not match contact' if hmac_identifier_conflict?
|
||||||
|
|
||||||
@contact_inbox.update(hmac_verified: true) if @contact_inbox.present?
|
@contact_inbox.update(hmac_verified: true) if @contact_inbox.present? && hmac_identifier_matches?
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_hmac?
|
def valid_hmac?
|
||||||
@@ -42,6 +43,24 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Rejects the request when the selected contact is already bound to a different identifier,
|
||||||
|
# so a valid (identifier, identifier_hash) pair cannot be replayed against another contact.
|
||||||
|
def hmac_identifier_conflict?
|
||||||
|
return false if @contact_inbox.blank?
|
||||||
|
|
||||||
|
contact_identifier = @contact_inbox.contact.identifier.to_s
|
||||||
|
contact_identifier.present? &&
|
||||||
|
!ActiveSupport::SecurityUtils.secure_compare(contact_identifier, params[:identifier].to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Only grants verified trust when the proven identifier matches the contact's own identifier.
|
||||||
|
# A blank contact identifier never grants trust, preventing replay onto anonymous contacts.
|
||||||
|
def hmac_identifier_matches?
|
||||||
|
contact_identifier = @contact_inbox.contact.identifier.to_s
|
||||||
|
contact_identifier.present? &&
|
||||||
|
ActiveSupport::SecurityUtils.secure_compare(contact_identifier, params[:identifier].to_s)
|
||||||
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -554,10 +554,11 @@ provideMessageContext({
|
|||||||
<Avatar v-bind="avatarInfo" :size="24" />
|
<Avatar v-bind="avatarInfo" :size="24" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="[grid-area:bubble] flex min-w-0"
|
class="[grid-area:bubble] flex"
|
||||||
:class="{
|
:class="{
|
||||||
'ltr:ml-8 rtl:mr-8 justify-end': orientation === ORIENTATION.RIGHT,
|
'ltr:ml-8 rtl:mr-8 justify-end': orientation === ORIENTATION.RIGHT,
|
||||||
'ltr:mr-8 rtl:ml-8': orientation === ORIENTATION.LEFT,
|
'ltr:mr-8 rtl:ml-8': orientation === ORIENTATION.LEFT,
|
||||||
|
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
|
||||||
}"
|
}"
|
||||||
@contextmenu="openContextMenu($event)"
|
@contextmenu="openContextMenu($event)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ const replyToPreview = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="text-sm min-w-0"
|
class="text-sm"
|
||||||
:class="[
|
:class="[
|
||||||
messageClass,
|
messageClass,
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
shared: &shared
|
shared: &shared
|
||||||
version: '4.14.1'
|
version: '4.14.0'
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *shared
|
<<: *shared
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@chatwoot/chatwoot",
|
"name": "@chatwoot/chatwoot",
|
||||||
"version": "4.14.1",
|
"version": "4.14.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"eslint": "eslint app/**/*.{js,vue}",
|
"eslint": "eslint app/**/*.{js,vue}",
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ RSpec.describe 'Microsoft Authorization API', type: :request do
|
|||||||
]
|
]
|
||||||
expect(params['scope']).to eq(expected_scope)
|
expect(params['scope']).to eq(expected_scope)
|
||||||
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback"])
|
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback"])
|
||||||
expect(url).not_to match(/(?:\?|&)prompt=/)
|
|
||||||
|
|
||||||
# Validate state parameter exists and can be decoded back to the account
|
# Validate state parameter exists and can be decoded back to the account
|
||||||
expect(params['state']).to be_present
|
expect(params['state']).to be_present
|
||||||
|
|||||||
@@ -48,4 +48,54 @@ RSpec.describe 'Public Inbox Contacts API', type: :request do
|
|||||||
expect(data['name']).to eq 'John Smith'
|
expect(data['name']).to eq 'John Smith'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'HMAC identifier binding' do
|
||||||
|
let!(:owner_contact) { create(:contact, account: api_channel.account, identifier: 'owner-identifier') }
|
||||||
|
let!(:owner_inbox) { create(:contact_inbox, contact: owner_contact, inbox: api_channel.inbox) }
|
||||||
|
let!(:victim_contact) { create(:contact, account: api_channel.account, identifier: 'victim-identifier', name: 'Victim') }
|
||||||
|
let!(:victim_inbox) { create(:contact_inbox, contact: victim_contact, inbox: api_channel.inbox) }
|
||||||
|
let(:valid_owner_hash) { OpenSSL::HMAC.hexdigest('sha256', api_channel.hmac_token, 'owner-identifier') }
|
||||||
|
|
||||||
|
it 'does not promote the victim contact inbox to hmac_verified when a valid pair is replayed against the victim source_id' do
|
||||||
|
begin
|
||||||
|
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{victim_inbox.source_id}",
|
||||||
|
params: { identifier: 'owner-identifier', identifier_hash: valid_owner_hash }
|
||||||
|
rescue StandardError
|
||||||
|
# the fix fails closed by raising; the security invariant is asserted below
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(victim_inbox.reload.hmac_verified).to be(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not mutate the victim contact when a valid pair is replayed against the victim source_id' do
|
||||||
|
begin
|
||||||
|
patch "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{victim_inbox.source_id}",
|
||||||
|
params: { identifier: 'owner-identifier', identifier_hash: valid_owner_hash, name: 'Hacked' }
|
||||||
|
rescue StandardError
|
||||||
|
# the fix fails closed by raising; the security invariant is asserted below
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(victim_contact.reload.name).to eq('Victim')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'still verifies the contact inbox when the owner presents a matching identifier pair' do
|
||||||
|
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{owner_inbox.source_id}",
|
||||||
|
params: { identifier: 'owner-identifier', identifier_hash: valid_owner_hash }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(owner_inbox.reload.hmac_verified).to be(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'verifies an API contact created without an identifier when a valid pair is supplied later' do
|
||||||
|
post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts"
|
||||||
|
|
||||||
|
anonymous_source_id = response.parsed_body['source_id']
|
||||||
|
anonymous_inbox = api_channel.inbox.contact_inboxes.find_by!(source_id: anonymous_source_id)
|
||||||
|
|
||||||
|
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{anonymous_source_id}",
|
||||||
|
params: { identifier: 'owner-identifier', identifier_hash: valid_owner_hash }
|
||||||
|
|
||||||
|
expect(anonymous_inbox.reload.hmac_verified).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user