diff --git a/Dockerfile b/Dockerfile index 9e51a29f2..fad85bcfe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,13 @@ FROM rails:4.2.5 MAINTAINER BioSistemika # additional dependecies -RUN apt-get update -qq && apt-get install -y default-jre-headless unison sudo graphviz --no-install-recommends && rm -rf /var/lib/apt/lists/* +RUN apt-get update -qq && \ + apt-get install -y \ + default-jre-headless \ + unison \ + sudo graphviz --no-install-recommends \ + sudo libfile-mimeinfo-perl && \ + rm -rf /var/lib/apt/lists/* # heroku tools RUN wget -O- https://toolbelt.heroku.com/install-ubuntu.sh | sh diff --git a/app/assets/javascripts/direct-upload.js b/app/assets/javascripts/direct-upload.js index 8d848fef7..59923b0ef 100644 --- a/app/assets/javascripts/direct-upload.js +++ b/app/assets/javascripts/direct-upload.js @@ -53,9 +53,9 @@ } /* - * The server checks if files are OK (correct file type, presence, - * size and spoofing) and only then generates posts for S3 server file - * uploading (each post for different size/style of the same file). + * The server checks if files are OK (presence, size and spoofing) + * and only then generates posts for S3 server file uploading + * (each post for different size/style of the same file). */ function fetchUploadSignature(ev, fileInput, file, signUrl) { var formData = new FormData(); @@ -69,17 +69,14 @@ contentType: false, error: function (xhr) { try { - var data = JSON.parse(xhr.responseText); - if (data.status === "error") { - // File error - var errMsg = jsonToValuesArray(data.errors); - renderFormError(ev, fileInput, errMsg); - } + // File error + var jsonData = $.parseJSON(xhr.responseText); + var errMsg = jsonToValuesArray(jsonData.errors); } catch(err) { // Connection error var errMsg = I18n.t("general.file.upload_failure"); - renderFormError(ev, fileInput, errMsg); } + renderFormError(ev, fileInput, errMsg); } }); } @@ -102,9 +99,15 @@ data: formData, processData: false, contentType: false, - error: function () { - // Connection error - var errMsg = I18n.t("general.file.upload_failure"); + error: function (xhr) { + try { + // File error + var $xmlData = $(xhr.responseText); + var errMsg = $xmlData.find("Message").text().strToErrorFormat(); + } catch(err) { + // Connection error + var errMsg = I18n.t("general.file.upload_failure"); + } renderFormError(ev, fileInput, errMsg); } }); diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index 149d83d95..1c56ef088 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb @@ -7,12 +7,8 @@ class AssetsController < ApplicationController def signature respond_to do |format| format.json { - asset = Asset.new(asset_params) if asset.errors.any? - # We need to validate, although 'new' already does it, so that - # asset's after_validation gets triggered, which modifies errors - asset.valid? render json: { status: 'error', errors: asset.errors diff --git a/app/models/asset.rb b/app/models/asset.rb index 2b8fad5cd..04b9582cb 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -42,9 +42,11 @@ class Asset < ActiveRecord::Base has_many :report_elements, inverse_of: :asset, dependent: :destroy has_one :asset_text_datum, inverse_of: :asset, dependent: :destroy - # Specific file errors propagate to "fire" error hash key, + # Specific file errors propagate to "file" error hash key, # so use just these errors after_validation :filter_paperclip_errors + # Needed because Paperclip validatates on creation + after_create :filter_paperclip_errors attr_accessor :file_content, :file_info, :preview_cached @@ -343,5 +345,4 @@ class Asset < ActiveRecord::Base self.file = data end - end diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 007d32e86..0076ab44a 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -57,3 +57,115 @@ Paperclip::Attachment.class_eval do Paperclip.io_adapters.for self end end + +module Paperclip + # Checks file for spoofing + class MediaTypeSpoofDetector + def spoofed? + if has_name? && has_extension? && (media_type_mismatch? || + mapping_override_mismatch?) + Paperclip.log("Content Type Spoof: Filename #{File.basename(@name)} "\ + "(#{supplied_content_type} from Headers, #{content_types_from_name} "\ + 'from Extension), content type discovered: '\ + "#{calculated_content_type}. See documentation to allow this "\ + 'combination.') + true + else + false + end + end + + private + + # Determine file content type from its name + def content_types_from_name + @content_types_from_name ||= + Paperclip.run('mimetype', '-b :file_name', file_name: @name).chomp + end + + # Determine file media type from its name + def media_types_from_name + @media_types_from_name ||= extract_media_type content_types_from_name + end + + # Determine file content type from mimetype command + def type_from_mimetype_command + @type_from_mimetype_command ||= + Paperclip.run('mimetype', '-b :file', file: @file.path).chomp + end + + # Determine file media type from mimetype command + def media_type_from_mimetype_command + @media_type_from_mimetype_command ||= + extract_media_type type_from_mimetype_command + end + + # Determine file content type from it's content (file and mimetype command) + def type_from_file_command + unless defined? @type_from_file_command + @type_from_file_command = + Paperclip.run('file', '-b --mime :file', file: @file.path) + .split(/[:;]\s+/).first + + if allowed_spoof_exception?(@type_from_file_command, + media_type_from_file_command) || + (@type_from_file_command.in?(%w(text/plain text/html)) && + media_type_from_mimetype_command.in?(%w(text application))) + # File content type is generalized, so rely on file extension for + # correct/more specific content type + @type_from_file_command = type_from_mimetype_command + end + end + @type_from_file_command + rescue Cocaine::CommandLineError + '' + end + + # Determine file media type from it's content (file and mimetype command) + def media_type_from_file_command + @media_type_from_file_command ||= + extract_media_type type_from_file_command + end + + def extract_media_type(content_type) + if content_type.empty? + '' + else + content_type.split('/').first + end + end + + # Checks file media type mismatch between file's name and header + def supplied_type_mismatch? + !allowed_spoof_exception?(supplied_content_type, supplied_media_type) && + media_types_from_name != supplied_media_type + end + + # Checks file media type mismatch between file's name and content + def calculated_type_mismatch? + !allowed_spoof_exception?(calculated_content_type, + calculated_media_type) && + media_types_from_name != calculated_media_type + end + + # Checks file content type mismatch between file's name and content + def mapping_override_mismatch? + !allowed_spoof_exception?(calculated_content_type, + calculated_media_type) && + content_types_from_name != calculated_content_type + end + + # Check if we have a file spoof exception which is allowed/safe + def allowed_spoof_exception?(content_type, media_type) + content_type == 'application/octet-stream' || + (content_type == 'inode/x-empty' && @file.size.zero?) || + (content_type == 'text/x-c' && + content_types_from_name == 'text/x-java') || + (media_type.in?(%w(image audio video)) && + media_type == media_types_from_name) || + (content_types_from_name.in? %W(#{} + text/plain + application/octet-stream)) + end + end +end