Sourced from rack's releases.
v3.2.6
Full Changelog: https://github.com/rack/rack/compare/v3.2.5...v3.2.6
Sourced from rack's changelog.
[3.2.6] - 2026-04-01
Security
- CVE-2026-34763 Root directory disclosure via unescaped regex interpolation in
Rack::Directory.- CVE-2026-34230 Avoid O(n^2) algorithm in
Rack::Utils.select_best_encodingwhich could lead to denial of service.- CVE-2026-32762 Forwarded header semicolon injection enables Host and Scheme spoofing.
- CVE-2026-26961 Raise error for multipart requests with multiple boundary parameters.
- CVE-2026-34786
Rack::Staticheader_rulesbypass via URL-encoded path mismatch.- CVE-2026-34831
Content-Lengthmismatch inRack::Fileserror responses.- CVE-2026-34826 Multipart byte range processing allows denial of service via excessive overlapping ranges.
- CVE-2026-34835
Rack::Requestaccepts invalid Host characters, enabling host allowlist bypass.- CVE-2026-34830
Rack::Sendfileheader-basedX-Accel-Mappingregex injection enables unauthorizedX-Accel-Redirect.- CVE-2026-34785
Rack::Staticprefix matching can expose unintended files under the static root.- CVE-2026-34829 Multipart parsing without
Content-Lengthheader allows unbounded chunked file uploads.- CVE-2026-34827 Multipart header parsing allows denial of service via escape-heavy quoted parameters.
- CVE-2026-26962 Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
e1f22fd
Bump patch version.31989fd
Fix typo in test.d268165
Fix test expectation.8f425de
Add Ruby v4.0 to the test matrix.bf83042
Drop EOL Rubies from external tests.d50c4d3
Implement OBS unfolding for multipart requests per RFC 5322 2.2.3bfb6914
Limit the number of quoted escapes during multipart parsingb3e5945
Add Content-Length size check in Rack::Multipart::Parser7a8f326
Fix root prefix bug in Rack::Statica57bc14
Only do a simple substitution on the x-accel-mapping pathsSourced from rack-session's changelog.
v2.1.2
- CVE-2026-39324 Don't fall back to unencrypted coder if encryptors are present.
504367b
Bump patch version.f43638c
Don't fall back to unencrypted coder if encryptors are present.dadcfe6
Bump actions/checkout from 4 to 5 (#54)4eb9ea8
Add top level session spec to validate existing formats.8f94577
Add rails to external tests.38ea47d
Allow the v2 encryptor to serialize messages with Marshal
(#44)43f2e3a
Fix compatibility with older Rubies.6a060b8
Support UTF-8 data when using the JSON serializer (#39)8ce0146
Fix auth_tag retrieval on JRuby (#32)7727185
Add AEAD encryption (#23)This is an HTML message with an inline image.
+
+
+
+
+------=_NextPart_001_0002--
+
+------=_NextPart_000_0001
+Content-Type: image/jpeg;
+ filename="image001.jpg"
+Content-Transfer-Encoding: base64
+Content-ID:
')
+
+ expect(helper_instance.send(:body_references_cid?, 'image001.jpg@test')).to be true
+ end
+ end
+
+ describe '#upload_inline_image' do
+ let(:mail_attachment) do
+ {
+ original: OpenStruct.new(cid: 'image001.jpg@test'),
+ blob: get_blob_for('spec/assets/avatar.png', 'image/png')
+ }
+ end
+ let(:helper_instance) { mailbox_helper_obj.new(conversation, processed_mail) }
+
+ it 'replaces percent-encoded CID references in HTML content' do
+ allow(Rails.application.routes.url_helpers).to receive(:url_for).and_return('/fake-image-url')
+ helper_instance.instance_variable_set(:@html_content, '
')
+
+ helper_instance.send(:upload_inline_image, mail_attachment)
+
+ html_content = helper_instance.instance_variable_get(:@html_content)
+ expect(html_content).to include('/fake-image-url"')
+ expect(html_content).not_to include('cid:')
+ end
+ end
+
+ describe '#add_attachments_to_message' do
+ let(:mail) { create_inbound_email_from_fixture('cid_inline_images_without_disposition.eml').mail }
+ let(:processed_mail) { MailPresenter.new(mail) }
+ let(:conversation) { create(:conversation) }
+ let(:helper_instance) { mailbox_helper_obj.new(conversation, processed_mail) }
+
+ before do
+ helper_instance.send(:create_message)
+ end
+
+ it 'detects inline image attachment by cid reference when Content-Disposition is missing' do
+ allow(Rails.application.routes.url_helpers).to receive(:url_for).and_return('/fake-image-url')
+ helper_instance.send(:add_attachments_to_message)
+
+ message = conversation.messages[0]
+
+ expect(message.attachments.count).to eq(0)
+
+ html_content = message.content_attributes[:email][:html_content][:full]
+
+ expect(html_content).to include('/fake-image-url"')
+ expect(html_content).not_to include('cid:')
+ end
+ end
end
From fe44b0714790722f4fbf70c962f3a96d5a095509 Mon Sep 17 00:00:00 2001
From: salmonumbrella <182032677+salmonumbrella@users.noreply.github.com>
Date: Wed, 6 May 2026 08:20:27 -0700
Subject: [PATCH 08/15] feat(companies): add company detail page (#14054)
---
app/javascript/dashboard/api/companies.js | 52 ++-
.../dashboard/api/specs/companies.spec.js | 142 +++----
.../Companies/CompaniesCard/CompaniesCard.vue | 52 ++-
.../Companies/CompaniesDetailsLayout.vue | 115 ++++++
.../CompaniesHeader/CompanyHeader.vue | 20 +-
.../components/CompanySortMenu.vue | 6 +
.../CompanyDetail/CompanyContactsSidebar.vue | 354 +++++++++++++++++
.../CompanyCustomAttributeItem.vue | 94 +++++
.../CompanyDetail/CompanyCustomAttributes.vue | 142 +++++++
.../CompanyDetail/CompanyProfileCard.vue | 203 ++++++++++
.../ConfirmCompanyDeleteDialog.vue | 45 +++
.../CustomAttributes/DateAttribute.vue | 2 +-
.../CustomAttributes/OtherAttribute.vue | 2 +-
.../components-next/sidebar/Sidebar.vue | 2 +-
.../i18n/locale/en/attributesMgmt.json | 8 +-
.../dashboard/i18n/locale/en/companies.json | 95 +++++
.../companies/pages/CompaniesIndex.vue | 19 +-
.../companies/pages/CompanyDetailView.vue | 236 ++++++++++++
.../routes/dashboard/companies/routes.js | 14 +
.../dashboard/settings/attributes/Index.vue | 5 +-
.../dashboard/store/modules/attributes.js | 5 +
.../modules/specs/attributes/getters.spec.js | 30 ++
app/javascript/dashboard/stores/companies.js | 358 +++++++++++++++++-
.../dashboard/stores/companies.spec.js | 213 +++++++++++
app/models/custom_attribute_definition.rb | 15 +-
config/routes.rb | 11 +
..._add_additional_attributes_to_companies.rb | 9 +
db/schema.rb | 3 +
.../accounts/companies/contacts_controller.rb | 87 +++++
.../api/v1/accounts/companies_controller.rb | 51 ++-
enterprise/app/models/company.rb | 18 +
enterprise/app/policies/company_policy.rb | 8 +
.../companies/contact_membership_service.rb | 15 +
.../accounts/companies/_company.json.jbuilder | 8 -
.../accounts/companies/avatar.json.jbuilder | 3 +
.../companies/contacts/_contact.json.jbuilder | 10 +
.../companies/contacts/create.json.jbuilder | 3 +
.../companies/contacts/index.json.jbuilder | 10 +
.../companies/contacts/search.json.jbuilder | 10 +
.../accounts/companies/create.json.jbuilder | 2 +-
.../destroy_custom_attributes.json.jbuilder | 3 +
.../v1/accounts/companies/index.json.jbuilder | 2 +-
.../accounts/companies/search.json.jbuilder | 2 +-
.../v1/accounts/companies/show.json.jbuilder | 2 +-
.../accounts/companies/update.json.jbuilder | 2 +-
.../api/v1/models/_company.json.jbuilder | 10 +
.../companies/contacts_controller_spec.rb | 78 ++++
.../v1/accounts/companies_controller_spec.rb | 67 ++++
.../custom_attribute_definition_spec.rb | 11 +
49 files changed, 2461 insertions(+), 193 deletions(-)
create mode 100644 app/javascript/dashboard/components-next/Companies/CompaniesDetailsLayout.vue
create mode 100644 app/javascript/dashboard/components-next/Companies/CompanyDetail/CompanyContactsSidebar.vue
create mode 100644 app/javascript/dashboard/components-next/Companies/CompanyDetail/CompanyCustomAttributeItem.vue
create mode 100644 app/javascript/dashboard/components-next/Companies/CompanyDetail/CompanyCustomAttributes.vue
create mode 100644 app/javascript/dashboard/components-next/Companies/CompanyDetail/CompanyProfileCard.vue
create mode 100644 app/javascript/dashboard/components-next/Companies/CompanyDetail/ConfirmCompanyDeleteDialog.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/companies/pages/CompanyDetailView.vue
create mode 100644 app/javascript/dashboard/stores/companies.spec.js
create mode 100644 db/migrate/20260422133000_add_additional_attributes_to_companies.rb
create mode 100644 enterprise/app/controllers/api/v1/accounts/companies/contacts_controller.rb
create mode 100644 enterprise/app/services/companies/contact_membership_service.rb
delete mode 100644 enterprise/app/views/api/v1/accounts/companies/_company.json.jbuilder
create mode 100644 enterprise/app/views/api/v1/accounts/companies/avatar.json.jbuilder
create mode 100644 enterprise/app/views/api/v1/accounts/companies/contacts/_contact.json.jbuilder
create mode 100644 enterprise/app/views/api/v1/accounts/companies/contacts/create.json.jbuilder
create mode 100644 enterprise/app/views/api/v1/accounts/companies/contacts/index.json.jbuilder
create mode 100644 enterprise/app/views/api/v1/accounts/companies/contacts/search.json.jbuilder
create mode 100644 enterprise/app/views/api/v1/accounts/companies/destroy_custom_attributes.json.jbuilder
create mode 100644 enterprise/app/views/api/v1/models/_company.json.jbuilder
create mode 100644 spec/enterprise/controllers/api/v1/accounts/companies/contacts_controller_spec.rb
diff --git a/app/javascript/dashboard/api/companies.js b/app/javascript/dashboard/api/companies.js
index 090b530c4..b98a59983 100644
--- a/app/javascript/dashboard/api/companies.js
+++ b/app/javascript/dashboard/api/companies.js
@@ -1,21 +1,12 @@
/* global axios */
import ApiClient from './ApiClient';
-export const buildCompanyParams = (page, sort) => {
- let params = `page=${page}`;
- if (sort) {
- params = `${params}&sort=${sort}`;
- }
- return params;
-};
-
-export const buildSearchParams = (query, page, sort) => {
- let params = `q=${encodeURIComponent(query)}&page=${page}`;
- if (sort) {
- params = `${params}&sort=${sort}`;
- }
- return params;
-};
+const buildParams = params =>
+ new URLSearchParams(
+ Object.entries(params).filter(
+ ([key, value]) => value !== undefined && (value !== '' || key === 'q')
+ )
+ ).toString();
class CompanyAPI extends ApiClient {
constructor() {
@@ -24,14 +15,41 @@ class CompanyAPI extends ApiClient {
get(params = {}) {
const { page = 1, sort = 'name' } = params;
- const requestURL = `${this.url}?${buildCompanyParams(page, sort)}`;
+ const requestURL = `${this.url}?${buildParams({ page, sort })}`;
return axios.get(requestURL);
}
search(query = '', page = 1, sort = 'name') {
- const requestURL = `${this.url}/search?${buildSearchParams(query, page, sort)}`;
+ const requestURL = `${this.url}/search?${buildParams({ q: query, page, sort })}`;
return axios.get(requestURL);
}
+
+ listContacts(id, page = 1) {
+ return axios.get(`${this.url}/${id}/contacts?${buildParams({ page })}`);
+ }
+
+ searchContacts(id, query = '', page = 1) {
+ const requestURL = `${this.url}/${id}/contacts/search?${buildParams({ q: query, page })}`;
+ return axios.get(requestURL);
+ }
+
+ createContact(id, payload) {
+ return axios.post(`${this.url}/${id}/contacts`, payload);
+ }
+
+ removeContact(id, contactId) {
+ return axios.delete(`${this.url}/${id}/contacts/${contactId}`);
+ }
+
+ destroyCustomAttributes(id, customAttributes) {
+ return axios.post(`${this.url}/${id}/destroy_custom_attributes`, {
+ custom_attributes: customAttributes,
+ });
+ }
+
+ destroyAvatar(id) {
+ return axios.delete(`${this.url}/${id}/avatar`);
+ }
}
export default new CompanyAPI();
diff --git a/app/javascript/dashboard/api/specs/companies.spec.js b/app/javascript/dashboard/api/specs/companies.spec.js
index 82fdc1c97..ca1d905de 100644
--- a/app/javascript/dashboard/api/specs/companies.spec.js
+++ b/app/javascript/dashboard/api/specs/companies.spec.js
@@ -1,7 +1,4 @@
-import companyAPI, {
- buildCompanyParams,
- buildSearchParams,
-} from '../companies';
+import companyAPI from '../companies';
import ApiClient from '../ApiClient';
describe('#CompanyAPI', () => {
@@ -9,7 +6,6 @@ describe('#CompanyAPI', () => {
expect(companyAPI).toBeInstanceOf(ApiClient);
expect(companyAPI).toHaveProperty('get');
expect(companyAPI).toHaveProperty('show');
- expect(companyAPI).toHaveProperty('create');
expect(companyAPI).toHaveProperty('update');
expect(companyAPI).toHaveProperty('delete');
expect(companyAPI).toHaveProperty('search');
@@ -32,111 +28,69 @@ describe('#CompanyAPI', () => {
window.axios = originalAxios;
});
- it('#get with default params', () => {
+ it('#get includes pagination and sorting params', () => {
companyAPI.get({});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/companies?page=1&sort=name'
);
});
- it('#get with page and sort params', () => {
- companyAPI.get({ page: 2, sort: 'domain' });
- expect(axiosMock.get).toHaveBeenCalledWith(
- '/api/v1/companies?page=2&sort=domain'
- );
- });
-
- it('#get with descending sort', () => {
- companyAPI.get({ page: 1, sort: '-created_at' });
- expect(axiosMock.get).toHaveBeenCalledWith(
- '/api/v1/companies?page=1&sort=-created_at'
- );
- });
-
- it('#search with query', () => {
- companyAPI.search('acme', 1, 'name');
- expect(axiosMock.get).toHaveBeenCalledWith(
- '/api/v1/companies/search?q=acme&page=1&sort=name'
- );
- });
-
- it('#search with special characters in query', () => {
+ it('#search encodes query params', () => {
companyAPI.search('acme & co', 2, 'domain');
expect(axiosMock.get).toHaveBeenCalledWith(
- '/api/v1/companies/search?q=acme%20%26%20co&page=2&sort=domain'
+ '/api/v1/companies/search?q=acme+%26+co&page=2&sort=domain'
);
});
- it('#search with descending sort', () => {
- companyAPI.search('test', 1, '-created_at');
- expect(axiosMock.get).toHaveBeenCalledWith(
- '/api/v1/companies/search?q=test&page=1&sort=-created_at'
- );
- });
-
- it('#search with empty query', () => {
+ it('#search keeps empty query param for backend validation', () => {
companyAPI.search('', 1, 'name');
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/companies/search?q=&page=1&sort=name'
);
});
- });
-});
-describe('#buildCompanyParams', () => {
- it('returns correct string with page only', () => {
- expect(buildCompanyParams(1)).toBe('page=1');
- });
-
- it('returns correct string with page and sort', () => {
- expect(buildCompanyParams(1, 'name')).toBe('page=1&sort=name');
- });
-
- it('returns correct string with different page', () => {
- expect(buildCompanyParams(3, 'domain')).toBe('page=3&sort=domain');
- });
-
- it('returns correct string with descending sort', () => {
- expect(buildCompanyParams(1, '-created_at')).toBe(
- 'page=1&sort=-created_at'
- );
- });
-
- it('returns correct string without sort parameter', () => {
- expect(buildCompanyParams(2, '')).toBe('page=2');
- });
-});
-
-describe('#buildSearchParams', () => {
- it('returns correct string with all parameters', () => {
- expect(buildSearchParams('acme', 1, 'name')).toBe(
- 'q=acme&page=1&sort=name'
- );
- });
-
- it('returns correct string with special characters', () => {
- expect(buildSearchParams('acme & co', 2, 'domain')).toBe(
- 'q=acme%20%26%20co&page=2&sort=domain'
- );
- });
-
- it('returns correct string with empty query', () => {
- expect(buildSearchParams('', 1, 'name')).toBe('q=&page=1&sort=name');
- });
-
- it('returns correct string without sort parameter', () => {
- expect(buildSearchParams('test', 1, '')).toBe('q=test&page=1');
- });
-
- it('returns correct string with descending sort', () => {
- expect(buildSearchParams('company', 3, '-created_at')).toBe(
- 'q=company&page=3&sort=-created_at'
- );
- });
-
- it('encodes special characters correctly', () => {
- expect(buildSearchParams('test@example.com', 1, 'name')).toBe(
- 'q=test%40example.com&page=1&sort=name'
- );
+ it('#destroyAvatar deletes the company avatar endpoint', () => {
+ companyAPI.destroyAvatar(1);
+ expect(axiosMock.delete).toHaveBeenCalledWith(
+ '/api/v1/companies/1/avatar'
+ );
+ });
+
+ it('#listContacts fetches company contacts', () => {
+ companyAPI.listContacts(1, 2);
+ expect(axiosMock.get).toHaveBeenCalledWith(
+ '/api/v1/companies/1/contacts?page=2'
+ );
+ });
+
+ it('#searchContacts encodes contact search params', () => {
+ companyAPI.searchContacts(1, 'jane & co', 3);
+ expect(axiosMock.get).toHaveBeenCalledWith(
+ '/api/v1/companies/1/contacts/search?q=jane+%26+co&page=3'
+ );
+ });
+
+ it('#createContact links a contact to the company', () => {
+ companyAPI.createContact(1, { contact_id: 2 });
+ expect(axiosMock.post).toHaveBeenCalledWith(
+ '/api/v1/companies/1/contacts',
+ { contact_id: 2 }
+ );
+ });
+
+ it('#removeContact unlinks a contact from the company', () => {
+ companyAPI.removeContact(1, 2);
+ expect(axiosMock.delete).toHaveBeenCalledWith(
+ '/api/v1/companies/1/contacts/2'
+ );
+ });
+
+ it('#destroyCustomAttributes removes company custom attributes', () => {
+ companyAPI.destroyCustomAttributes(1, ['plan']);
+ expect(axiosMock.post).toHaveBeenCalledWith(
+ '/api/v1/companies/1/destroy_custom_attributes',
+ { custom_attributes: ['plan'] }
+ );
+ });
});
});
diff --git a/app/javascript/dashboard/components-next/Companies/CompaniesCard/CompaniesCard.vue b/app/javascript/dashboard/components-next/Companies/CompaniesCard/CompaniesCard.vue
index 4603521eb..fe4385bbb 100644
--- a/app/javascript/dashboard/components-next/Companies/CompaniesCard/CompaniesCard.vue
+++ b/app/javascript/dashboard/components-next/Companies/CompaniesCard/CompaniesCard.vue
@@ -1,7 +1,7 @@
+ {{ t('COMPANIES.DETAIL.ATTRIBUTES.NO_ATTRIBUTES') }} +
++ {{ t('COMPANIES.DETAIL.ATTRIBUTES.EMPTY_STATE') }} +
+ diff --git a/app/javascript/dashboard/components-next/Companies/CompanyDetail/CompanyProfileCard.vue b/app/javascript/dashboard/components-next/Companies/CompanyDetail/CompanyProfileCard.vue new file mode 100644 index 000000000..1ffbf0c18 --- /dev/null +++ b/app/javascript/dashboard/components-next/Companies/CompanyDetail/CompanyProfileCard.vue @@ -0,0 +1,203 @@ + + + ++ {{ t('COMPANIES.DETAIL.AVATAR.UPDATING') }} +
++ {{ t('COMPANIES.DETAIL.EMPTY_STATE.SUBTITLE') }} +
+{{ $t('BULK_ACTION.AGENT_LIST_LOADING') }}
-- {{ - $t('BULK_ACTION.ASSIGN_CONFIRMATION_LABEL', { - conversationCount, - conversationLabel, - }) - }} - - {{ selectedAgent.name }} - - ? -
-- {{ - $t('BULK_ACTION.UNASSIGN_CONFIRMATION_LABEL', { - conversationCount, - conversationLabel, - }) - }} -
-