Merge from docusealco/wip

master 3.2.6
Alex Turchyn 8 hours ago committed by GitHub
commit 47c090e1f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -152,11 +152,7 @@ jobs:
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
yarn install
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260813/pdfium-musl-$(uname -m).zip"
case "$(uname -m)" in
x86_64) echo "c5c7dde243ecb66ab0819c8193515ef38ad53549fe260f3c2dfd93ea56eda2e7 pdfium-linux.zip" ;;
aarch64) echo "64c4483449b1b4dccc696ad0c5c96e0b7f74dcc57b4f23c676b7a70671b0bbb5 pdfium-linux.zip" ;;
esac | sha256sum -c -
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-musl-$(uname -m).zip"
unzip -q pdfium-linux.zip -d /tmp/pdfium-linux
cp /tmp/pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
rm -rf pdfium-linux.zip /tmp/pdfium-linux

@ -9,10 +9,10 @@ RUN apk --no-cache add wget unzip && \
wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \
wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260813/pdfium-musl-$(uname -m).zip" && \
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260920/pdfium-musl-$(uname -m).zip" && \
case "$(uname -m)" in \
x86_64) echo "c5c7dde243ecb66ab0819c8193515ef38ad53549fe260f3c2dfd93ea56eda2e7 pdfium-linux.zip" ;; \
aarch64) echo "64c4483449b1b4dccc696ad0c5c96e0b7f74dcc57b4f23c676b7a70671b0bbb5 pdfium-linux.zip" ;; \
x86_64) echo "bbca8a648dbd1ba81f9c6c223f21dc394cf1ad381c6c17d60c6dcc4a7d3a2ae0 pdfium-linux.zip" ;; \
aarch64) echo "73b72ed79b9bfdd494dee7e863482c24971a8d4b11f6cd1289aa170d31304a1f pdfium-linux.zip" ;; \
esac | sha256sum -c - && \
mkdir -p /pdfium-linux && \
unzip -q pdfium-linux.zip -d /pdfium-linux

@ -23,7 +23,7 @@ module Api
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase)
if FilenameUtils.dangerous_extension(blob.filename)
Rollbar.error('Dangerous extension') if defined?(Rollbar)
return head :unprocessable_content

@ -19,7 +19,7 @@ module Api
return head :not_found unless blob
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase)
if FilenameUtils.dangerous_extension(blob.filename)
Rollbar.error('Dangerous extension') if defined?(Rollbar)
return head :unprocessable_content

@ -46,6 +46,8 @@ module Mcp
authorize!(:create, @template)
Templates.maybe_assign_access(@template)
if mcp_params['url'].present?
tempfile = Tempfile.new
tempfile.binmode

@ -26,8 +26,9 @@ class PreviewDocumentPageController < ActionController::API
@template = attachment.record
preview_image = attachment.preview_images.joins(:blob)
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
preview_image =
attachment.preview_images.joins(:blob)
.find_by(blob: { filename: ["#{params[:id].to_i}.png", "#{params[:id].to_i}.jpg"] })
if preview_image
return redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time),

@ -11,11 +11,9 @@ class UserInitialsController < ApplicationController
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
extension = File.extname(file.original_filename).delete_prefix('.').downcase
extension = FilenameUtils.dangerous_extension(file.original_filename)
if Submitters::DANGEROUS_EXTENSIONS.include?(extension)
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
end
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed." if extension
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,

@ -11,11 +11,9 @@ class UserSignaturesController < ApplicationController
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank?
extension = File.extname(file.original_filename).delete_prefix('.').downcase
extension = FilenameUtils.dangerous_extension(file.original_filename)
if Submitters::DANGEROUS_EXTENSIONS.include?(extension)
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
end
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed." if extension
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,

@ -353,7 +353,7 @@ function buildExtensions ({ Node, Mark, Extension, Plugin, Decoration, Decoratio
const buildDecorations = (doc) => {
const decorations = []
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g
const regex = /\{\{?[^{}\n]+\}\}?/g
doc.descendants((node, pos) => {
if (!node.isText) return

@ -150,7 +150,7 @@ export default actionable(targetable(class extends HTMLElement {
const buildDecorations = (doc) => {
const decorations = []
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g
const regex = /\{\{?[^{}\n]+\}\}?/g
doc.descendants((node, pos) => {
if (!node.isText) return

@ -3,7 +3,7 @@
<%= svg_icon('discount_check_filled', class: 'w-6 h-6 shrink-0 text-green-500') %>
<span class="flex items-center">
<span class="whitespace-nowrap"><%= t('docuseal_trusted_signature') %></span>
<div class="tooltip tooltip-top ml-1 shrink-0" data-tip="<%= t('sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature') %>">
<div class="tooltip tooltip-top ml-1 shrink-0 hidden md:flex" data-tip="<%= t('sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature') %>">
<%= svg_icon('circle_question', class: 'w-4 h-4 stroke-1 shrink-0') %>
</div>
</span>
@ -14,7 +14,7 @@
</a>
</td>
<td>
<div class="tooltip" data-tip="<%= t('unlock_with_docuseal_pro') %>">
<div class="md:tooltip" data-tip="<%= t('unlock_with_docuseal_pro') %>">
<%= button_to settings_esign_path, method: :put, params: { name: Docuseal::AATL_CERT_NAME }, class: 'btn btn-outline btn-neutral btn-xs whitespace-nowrap', title: t('make_default'), disabled: true do %>
<%= t('make_default') %>
<% end %>

@ -120,7 +120,7 @@
<span>
<%= t('timeserver_url') %>
</span>
<span class="tooltip" data-tip="<%= t('url_of_the_trusted_rfc_3161_timeserver_to_be_used_to_generate_timestamp_signatures') %>">
<span class="tooltip hidden md:flex" data-tip="<%= t('url_of_the_trusted_rfc_3161_timeserver_to_be_used_to_generate_timestamp_signatures') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</span>

@ -6,12 +6,10 @@
<%= t('mcp_server') %>
</h1>
<div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center">
<div class="tooltip">
<%= link_to new_settings_mcp_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_token') %></span>
<% end %>
</div>
<%= link_to new_settings_mcp_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_token') %></span>
<% end %>
</div>
</div>
<% if @mcp_token.present? %>

@ -12,7 +12,7 @@
<div class="form-control">
<div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents') %>">
<span class="tooltip tooltip-right hidden md:flex" data-tip="<%= t('completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>

@ -12,7 +12,7 @@
<div class="form-control">
<div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign') %>">
<span class="tooltip tooltip-right hidden md:flex" data-tip="<%= t('signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>

@ -12,7 +12,7 @@
<div class="form-control">
<div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties') %> <%= t('use_following_placeholders_text_') %> <%= AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].map { |v| "{#{v}}" }.join(', ') %>">
<span class="tooltip tooltip-right hidden md:flex" data-tip="<%= t('notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties') %> <%= t('use_following_placeholders_text_') %> <%= AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].map { |v| "{#{v}}" }.join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>

@ -36,7 +36,7 @@
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
</span>
<% if (device = DetectBrowserDevice.call(event.data['ua'])) %>
<span class="tooltip tooltip-top" data-tip="<%= t(device) %>">
<span class="md:tooltip tooltip-top" data-tip="<%= t(device) %>">
<%= svg_icon("device_#{device}", class: 'w-4 h-4') %>
</span>
<% end %>

@ -7,7 +7,7 @@
<%= f.label :preserve_order, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :preserve_order, id: uuid, class: 'base-checkbox', checked: last_submission&.submitters_order.in?(['preserved', nil]) %>
<span class="label"><%= t('preserve_order') %></span>
<span class="tooltip" data-tip="<%= t('when_checked_notifications_will_be_sent_to_the_second_party_once_the_form_is_completed_by_the_previous_party_uncheck_this_option_to_send_notifications_to_all_parties_simultaneously_right_away') %>">
<span class="tooltip hidden md:flex" data-tip="<%= t('when_checked_notifications_will_be_sent_to_the_second_party_once_the_form_is_completed_by_the_previous_party_uncheck_this_option_to_send_notifications_to_all_parties_simultaneously_right_away') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
<% end %>

@ -56,7 +56,7 @@
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to t('learn_more'), console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<script src=&quot;<%= Docuseal::CDN_URL %>/js/form.js&quot;></script>
<clipboard-copy data-text="<script src=&quot;<%= Docuseal.multitenant? ? "#{Docuseal::CDN_URL}/js/form.js" : embed_script_url(filename: 'form.js') %>&quot;></script>
<docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form>
">
@ -78,7 +78,7 @@
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal::CDN_URL %>/js/form.js"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal.multitenant? ? "#{Docuseal::CDN_URL}/js/form.js" : embed_script_url(filename: 'form.js') %>"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span>
<span style="color: #f4bf75">&lt;docuseal-form</span> <span style="color: #6a9fb5">data-src=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span><span style="color: #f4bf75">&gt;</span><span style="color: #f4bf75">&lt;/docuseal-form&gt;</span>
</code></pre>

@ -35,14 +35,14 @@
<div class="flex items-center space-x-4">
<span class="flex flex-col md:flex-row md:items-center gap-3">
<% if submission.expired? && !submitter.completed_at? && !submitter.declined_at? %>
<div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<div class="md:tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
<%= t('expired') %>
</span>
</div>
<% else %>
<a href="<%= submission_path(submission) %>" class="flex z-[1]">
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold tooltip" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold md:tooltip" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<%= t(submitter.status) %>
</span>
</a>
@ -125,7 +125,7 @@
</span>
</a>
<% elsif submission.expired? %>
<div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<div class="md:tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
<%= t('expired') %>
</span>

@ -5,7 +5,7 @@
<%= f.hidden_field :parent_name, value: @template.folder.parent_folder&.name || @template.folder.name %>
<toggle-visible data-element-ids="<%= %w[folder_form subfolder_form].to_json %>" class="block relative" data-focus-id="name">
<div class="flex items-center justify-between mb-2.5">
<label for="is_root_folder" class="flex items-center tooltip tooltip-right pr-2 group" data-tip="<%= t('change_parent_folder') %>">
<label for="is_root_folder" class="flex items-center md:tooltip tooltip-right pr-2 group" data-tip="<%= t('change_parent_folder') %>">
<%= check_box_tag :is_root_folder, 'folder_form', data: { action: 'change:toggle-visible#trigger' }, class: 'hidden' %>
<span class="flex items-center mt-1">
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0 group-hover:hidden mr-1') %>

@ -12,7 +12,7 @@
<div class="flex justify-between">
<div class="flex items-center">
<%= ff.label :completed_notification_email_subject, t('email_subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties') %>">
<span class="tooltip tooltip-right hidden md:flex" data-tip="<%= t('notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>

@ -12,7 +12,7 @@
<div class="flex justify-between">
<div class="flex items-center">
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents') %>">
<span class="tooltip tooltip-right hidden md:flex" data-tip="<%= t('completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>

@ -18,7 +18,7 @@
<div class="flex justify-between">
<div class="flex items-center">
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign') %>">
<span class="tooltip tooltip-right hidden md:flex" data-tip="<%= t('signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>

@ -12,7 +12,7 @@
<div class="flex justify-between">
<div class="flex items-center">
<%= ff.label :invitation_view_email_subject, t('email_subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('view_only_email_sent_to_recipients_that_need_to_only_review_the_documents') %>">
<span class="tooltip tooltip-right hidden md:flex" data-tip="<%= t('view_only_email_sent_to_recipients_that_need_to_only_review_the_documents') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>

@ -10,7 +10,7 @@
<% end %>
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
<% if @template.preferences&.dig('require_email_2fa') || @template.preferences&.dig('require_phone_2fa') %>
<label for="template_shared_link" class="tooltip tooltip-bottom flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300" data-tip="<%= t(:templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link) %>">
<label for="template_shared_link" class="md:tooltip tooltip-bottom flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300" data-tip="<%= t(:templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link) %>">
<span><%= t('enable_shared_link') %></span>
<%= check_box_tag 'shared_link', 'true', false, disabled: true, class: 'toggle' %>
</label>

@ -24,7 +24,7 @@
<% end %>
<% end %>
<% else %>
<div class="tooltip" data-tip="<%= t('contact_your_administrator_to_add_new_users') %>">
<div class="md:tooltip" data-tip="<%= t('contact_your_administrator_to_add_new_users') %>">
<%= link_to '#', class: 'btn btn-primary btn-md gap-2 w-full md:w-fit btn-disabled', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_user') %></span>

@ -0,0 +1,21 @@
# frozen_string_literal: true
module FilenameUtils
DANGEROUS_EXTENSIONS = %w[
exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp
hta cpl jar app deb rpm dmg pkg mpkg dll so dylib sys
inf reg ps1 psm1 psd1 ps1xml psc1 pssc vb vba
sh bash zsh fish run out bin elf gadget workflow lnk scf
url desktop application action apk ipa xap appx
appxbundle msix msixbundle diagcab diagpkg msc ocx
drv ins isp mst paf prf shb shs slk ws wsc inf1 inf2
].freeze
DANGEROUS_EXTENSIONS_REGEXP = /\.(#{Regexp.union(DANGEROUS_EXTENSIONS).source})\W*\z/i
module_function
def dangerous_extension(filename)
ActiveStorage::Filename.wrap(filename).sanitized[DANGEROUS_EXTENSIONS_REGEXP, 1]&.downcase
end
end

@ -1,23 +1,33 @@
# frozen_string_literal: true
module ImageUtils
PNG_REGEXP = %r{\Aimage/(?:png|apng|vnd\.mozilla\.apng)\z}
JPEG_REGEXP = %r{\Aimage/(?:jpeg|jpg|pjpeg)\z}
ICO_REGEXP = %r{\Aimage/(?:x-icon|vnd\.microsoft\.icon)\z}
BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z}
UnsupportedFormat = Class.new(StandardError)
module_function
def load_vips(data, content_type: nil, autorot: false)
content_type ||= Marcel::MimeType.for(data)
if ICO_REGEXP.match?(content_type)
LoadIco.call(data)
elsif BMP_REGEXP.match?(content_type)
LoadBmp.call(data)
else
image = Vips::Image.new_from_buffer(data, '')
autorot ? image.autorot : image
end
image =
case content_type
when PNG_REGEXP
Vips::Image.pngload_buffer(data)
when JPEG_REGEXP
Vips::Image.jpegload_buffer(data)
when ICO_REGEXP
LoadIco.call(data)
when BMP_REGEXP
LoadBmp.call(data)
else
raise UnsupportedFormat, content_type.to_s
end
autorot ? image.autorot : image
end
def blank?(image)

@ -109,6 +109,7 @@ class Pdfium
attach_function :FPDF_GetLastError, [], :ulong
attach_function :FPDF_GetTrailerEnds, %i[FPDF_DOCUMENT pointer ulong], :ulong
attach_function :FPDF_DocumentHasValidCrossReferenceTable, [:FPDF_DOCUMENT], :int
attach_function :FPDF_HasOnlyDSSChanges, %i[FPDF_DOCUMENT FPDF_DOCUMENT], :int
attach_function :FPDF_GetSecurityHandlerRevision, [:FPDF_DOCUMENT], :int
attach_function :FPDF_GetFormType, [:FPDF_DOCUMENT], :int
@ -581,6 +582,10 @@ class Pdfium
@page_rotations[page_index] ||= Pdfium.FPDFPage_GetRotationRaw(@document_ptr, page_index)
end
def reset_page_rotation(page_index)
@page_rotations.delete(page_index)
end
def encrypted?
Pdfium.FPDF_GetSecurityHandlerRevision(@document_ptr) >= 0
end
@ -771,6 +776,10 @@ class Pdfium
Pdfium.FPDF_DocumentHasValidCrossReferenceTable(@document_ptr) == 1
end
def only_dss_changes?(signed_document)
Pdfium.FPDF_HasOnlyDSSChanges(@document_ptr, signed_document.document_ptr) == 1
end
def annot_count(page_index)
@annot_counts[page_index] ||= Pdfium.FPDFPage_GetAnnotCountRaw(@document_ptr, page_index)
end
@ -1010,6 +1019,7 @@ class Pdfium
Pdfium.FPDFPage_SetRotation(@page_ptr, value)
@document.reset_page_size(@page_index)
@document.reset_page_rotation(@page_index)
@rotation = value
end

@ -16,16 +16,6 @@ module Submitters
MaliciousFileExtension = Class.new(StandardError)
ParamsError = Class.new(StandardError)
DANGEROUS_EXTENSIONS = Set.new(%w[
exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp
hta cpl jar app deb rpm dmg pkg mpkg dll so dylib sys
inf reg ps1 psm1 psd1 ps1xml psc1 pssc bat cmd vb vba
sh bash zsh fish run out bin elf gadget workflow lnk scf
url desktop application action workflow apk ipa xap appx
appxbundle msix msixbundle diagcab diagpkg cpl msc ocx
drv scr ins isp mst paf prf shb shs slk ws wsc inf1 inf2
].freeze)
FILES_TTL = 5.minutes
module_function
@ -127,11 +117,9 @@ module Submitters
def create_attachment!(submitter, file, metadata: {})
raise ParamsError, 'file param is missing' if file.blank?
extension = File.extname(file.original_filename).delete_prefix('.').downcase
extension = FilenameUtils.dangerous_extension(file.original_filename)
if DANGEROUS_EXTENSIONS.include?(extension)
raise MaliciousFileExtension, "File type '.#{extension}' is not allowed."
end
raise MaliciousFileExtension, "File type '.#{extension}' is not allowed." if extension
blob = ActiveStorage::Blob.create_and_upload!(io: file.tap(&:rewind).open,
filename: file.original_filename,

@ -247,7 +247,7 @@ module Submitters
detected_extensions = Marcel::TYPE_EXTS[mime_type].to_a.map(&:downcase)
if detected_extensions.any? { |e| Submitters::DANGEROUS_EXTENSIONS.include?(e) }
if detected_extensions.any? { |e| FilenameUtils::DANGEROUS_EXTENSIONS.include?(e) }
raise InvalidDefaultValue, "File type '.#{detected_extensions.first}' is not allowed."
end
@ -276,11 +276,9 @@ module Submitters
def find_or_create_blob_from_url(account, url)
filename = Addressable::URI.parse(url).path.split('/').last.to_s
extension = File.extname(filename).delete_prefix('.').downcase
extension = FilenameUtils.dangerous_extension(filename)
if Submitters::DANGEROUS_EXTENSIONS.include?(extension)
raise InvalidDefaultValue, "File type '.#{extension}' is not allowed."
end
raise InvalidDefaultValue, "File type '.#{extension}' is not allowed." if extension
cache_key = [account.id, url].join(':')
checksum = CHECKSUM_CACHE_STORE.fetch(cache_key)

@ -10,20 +10,22 @@ module VerifyPdfSignature
module_function
def call(io, trusted_certs)
Pdfium::Document.open_io(io) do |document|
signatures = document.signatures.select { |e| e.byte_range.any?(&:positive?) && e.contents.present? }
next [] if signatures.blank?
verified_signatures = signatures.select { |e| verified_signature?(e, io, trusted_certs) }
trusted_signatures = verified_signatures.select { |e| trusted_signature?(e, trusted_certs) }
last_signature = (trusted_signatures.presence || verified_signatures).max_by(&:signed_end)
has_unsigned_changes = last_signature && unsigned_changes?(document, io, last_signature.signed_end)
signatures.map do |signature|
build_signature(signature, trusted_certs,
verified: verified_signatures.include?(signature),
has_unsigned_changes: has_unsigned_changes && signature == last_signature)
Pdfium.with_instance do
Pdfium::Document.open_io(io) do |document|
signatures = document.signatures.select { |e| e.byte_range.any?(&:positive?) && e.contents.present? }
next [] if signatures.blank?
verified_signatures = signatures.select { |e| verified_signature?(e, io, trusted_certs) }
trusted_signatures = verified_signatures.select { |e| trusted_signature?(e, trusted_certs) }
last_signature = (trusted_signatures.presence || verified_signatures).max_by(&:signed_end)
has_unsigned_changes = last_signature && unsigned_changes?(document, io, last_signature.signed_end)
signatures.map do |signature|
build_signature(signature, trusted_certs,
verified: verified_signatures.include?(signature),
has_unsigned_changes: has_unsigned_changes && signature == last_signature)
end
end
end
end
@ -31,11 +33,26 @@ module VerifyPdfSignature
def verified_signature?(signature, io, trusted_certs)
return false unless covers_signed_revision?(signature, io)
verify_contents(OpenSSL::PKCS7.new(signature.contents), signed_data(io, signature.byte_range), trusted_certs)
rescue OpenSSL::PKCS7::PKCS7Error
pkcs7 = OpenSSL::PKCS7.new(signature.contents)
if signature.sub_filter == 'ETSI.RFC3161'
verify_timestamp(pkcs7, signed_data(io, signature.byte_range))
else
verify_contents(pkcs7, signed_data(io, signature.byte_range), trusted_certs)
end
rescue OpenSSL::PKCS7::PKCS7Error, OpenSSL::Timestamp::TimestampError
false
end
def verify_timestamp(pkcs7, signed_data)
return false unless pkcs7.verify(pkcs7.certificates, OpenSSL::X509::Store.new, nil,
OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::BINARY)
token_info = OpenSSL::Timestamp::TokenInfo.new(pkcs7.data)
token_info.message_imprint == OpenSSL::Digest.digest(token_info.algorithm, signed_data)
end
def build_signature(signature, trusted_certs, verified:, has_unsigned_changes:)
pkcs7 = OpenSSL::PKCS7.new(signature.contents)
@ -173,27 +190,8 @@ module VerifyPdfSignature
io.seek(0)
Pdfium::Document.open_bytes(io.read(signed_end)) do |signed_document|
next true unless signed_document.valid_cross_reference_table?
serialized_document(signed_document) != serialized_document(document)
end
end
def serialized_document(document)
pages = (0...document.page_count).map do |index|
page = document.get_page(index)
objects = page.objects.map { |object| [*object.to_a, image_digest(page, object)] }
[page.rotation, objects, page.annotations, page.text]
!document.only_dss_changes?(signed_document)
end
[pages, document.bookmarks]
end
def image_digest(page, object)
return unless object.image?
Digest::SHA256.hexdigest(page.extract_image_bitmap(object.object_ptr)[:data])
end
def signed_data(io, byte_range)

Loading…
Cancel
Save