From 6284320777475b4564d695dbb669ec6ffa1933bd Mon Sep 17 00:00:00 2001 From: Rory Quinlan Date: Wed, 26 Oct 2016 14:44:15 +0200 Subject: [PATCH 1/2] initial image validation outline, untested and example values --- lib/code_corps/validators/image_validator.ex | 144 +++++++++++++++++++ web/uploaders/organization_icon.ex | 21 ++- web/uploaders/project_icon.ex | 21 ++- web/uploaders/user_photo.ex | 22 ++- 4 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 lib/code_corps/validators/image_validator.ex diff --git a/lib/code_corps/validators/image_validator.ex b/lib/code_corps/validators/image_validator.ex new file mode 100644 index 000000000..e7e6b1910 --- /dev/null +++ b/lib/code_corps/validators/image_validator.ex @@ -0,0 +1,144 @@ +defmodule CodeCorps.Validators.ImageStats do + @moduledoc """ + Struct for image stats needed for filtering + """ + @magnitudes [:bytes, :kilobytes, :megabytes, :gigabytes, :terabytes] + + @doc """ + """ + defstruct [ + filetype: nil, + bytes: nil, + width: nil, + height: nil + ] + + def aspect_ratio(%CodeCorps.Validators.ImageStats{width: width, height: height}) + when is_number(width) and is_number(height) do + width / height + end + def aspect_ratio(_image), do: nil + + def size_in(magnitude, %CodeCorps.Validators.ImageStats{bytes: bytes}) + when magnitude in @magnitudes and is_number(bytes) do + calculate_size_in(@magnitudes, magnitude, bytes) + end + def size_in(_magnitude, _image), do: nil + + defp calculate_size_in([current_magnitude | tail], desired_magnitude, count) do + if current_magnitude == desired_magnitude do + count + else + calculate_size_in(tail, desired_magnitude, count / 1024) + end + end +end + +defmodule CodeCorps.Validators.ImageValidator do + @moduledoc """ + Used for validating uploaded images for height, width, + aspect ratio, and filesize. + """ + alias CodeCorps.Validators.ImageStats + + @png_signature <<0x89, "PNG\r\n", 0x1A, "\n">> + @jpg_start_signature 0xFFD8 + @jpg_end_signature 0xFFD9 + @gif_89_signature "GIF89a" + @gif_87_signature "GIF87a" + @ihdr_label "IHDR" + + @doc """ + """ + def find_image_stats(image_binary) do + image_stats = parse_image_stats(image_binary) + if image_stats == nil do + nil + else + # this is slightly smaller than the size of + # the file when saved to disk, but close enough + image_bytes = byte_size(image_binary) + %ImageStats{ + bytes: image_bytes, + height: image_stats.height, + width: image_stats.width, + filetype: image_stats.filetype + } + end + end + + defp parse_image_stats(@png_signature <> << _length::big-integer-size(32), + @ihdr_label, + width::big-integer-size(32), + height::big-integer-size(32), + _remainder::binary >> ) do + %{ filetype: :png, + width: width, + height: height } + end + + defp parse_image_stats(<< @gif_89_signature, + width::little-integer-size(16), + height::little-integer-size(16), + _remainder::binary >>) do + %{ filetype: :gif, + width: width, + height: height } + end + + defp parse_image_stats(<< @gif_87_signature, + width::little-integer-size(16), + height::little-integer-size(16), + _remainder::binary >>) do + %{ filetype: :gif, + width: width, + height: height } + end + + # jpeg images will + defp parse_image_stats(<< 0xFF, 0xD8, image_binary::binary >>) do + pieces_if_baseline = String.split(image_binary, << 0xFF, 0xC0 >>) + pieces_if_progressive = String.split(image_binary, << 0xFF, 0xC2 >>) + len_func = &Enum.reduce(&1, 0, fn(_val, acc) -> acc + 1 end) + baseline_piece_count = len_func.(pieces_if_baseline) + progressive_piece_count = len_func.(pieces_if_progressive) + if baseline_piece_count == progressive_piece_count == 1 do + # fail if neither baseline or progressive indicators are present + nil + else + # otherwise, go by the more frequent indicator + # although they should be mutually exclusive + jpeg_pieces = if baseline_piece_count > progressive_piece_count do + pieces_if_baseline + else + pieces_if_progressive + end + {height, width} = parse_jpeg_pieces(jpeg_pieces) + %{ filetype: :jpg, + width: width, + height: height } + end + end + defp parse_image_stats(_image_binary), do: nil + + # for jpegs, it's not easy to tell which size height and width refers to the base image + # as opposed to the thumbnail(s), so we'll just go by the biggest one we find + defp parse_jpeg_pieces([ _prefix_piece | tail ]), do: parse_jpeg_pieces(tail, 0, 0, 0) + defp parse_jpeg_pieces([], height, width, _area), do: {height, width} + defp parse_jpeg_pieces([ current_piece | tail ], height, width, area) do + << _skipped_stats::size(24), + current_height::little-integer-size(16), + current_width::little-integer-size(16), + _remainder::binary >> = current_piece + + current_area = current_height * current_width + {height, width} = if current_area > area do + {current_height, current_width} + else + {height, width} + end + + parse_jpeg_pieces(tail, height, width, area) + end +end + diff --git a/web/uploaders/organization_icon.ex b/web/uploaders/organization_icon.ex index 4e0f57ec8..64594dd32 100644 --- a/web/uploaders/organization_icon.ex +++ b/web/uploaders/organization_icon.ex @@ -1,18 +1,31 @@ defmodule CodeCorps.OrganizationIcon do use Arc.Definition - # Include ecto support (requires package arc_ecto installed): use Arc.Ecto.Definition + alias CodeCorps.Validators.ImageValidator + alias CodeCorps.Validators.ImageStats @versions [:original, :large, :thumb] - @acl :public_read - @icon_color_generator Application.get_env(:code_corps, :icon_color_generator) + @max_filesize_MB 16 + @max_height 10_000 + @max_width 10_000 + @max_aspect_ratio 4 + @min_aspect_ratio 0.25 # Whitelist file extensions: def validate({file, _}) do - ~w(.jpg .jpeg .gif .png) |> Enum.member?(Path.extname(file.file_name)) + file_extension = Path.extname(file.file_name) + if ~w(.jpg .jpeg .gif .png) |> Enum.member?(file_extension) do + image = File.read!(file) + image_stats = ImageValidator.find_image_stats(image) + image_stats != nil && ImageStats.size_in(:megabytes, image_stats) > @max_filesize_MB + && image_stats.height <= @max_height && image_stats.width <= @max_width + && @max_aspect_ratio >= ImageStats.aspect_ratio(image_stats) >= @min_aspect_ratio + else + false + end end # Large transformation diff --git a/web/uploaders/project_icon.ex b/web/uploaders/project_icon.ex index 4a2d3b4f2..4fee7b8fc 100644 --- a/web/uploaders/project_icon.ex +++ b/web/uploaders/project_icon.ex @@ -1,18 +1,31 @@ defmodule CodeCorps.ProjectIcon do use Arc.Definition - # Include ecto support (requires package arc_ecto installed): use Arc.Ecto.Definition + alias CodeCorps.Validators.ImageValidator + alias CodeCorps.Validators.ImageStats @versions [:original, :large, :thumb] - @acl :public_read - @icon_color_generator Application.get_env(:code_corps, :icon_color_generator) + @max_filesize_MB 16 + @max_height 10_000 + @max_width 10_000 + @max_aspect_ratio 4 + @min_aspect_ratio 0.25 # Whitelist file extensions: def validate({file, _}) do - ~w(.jpg .jpeg .gif .png) |> Enum.member?(Path.extname(file.file_name)) + file_extension = Path.extname(file.file_name) + if ~w(.jpg .jpeg .gif .png) |> Enum.member?(file_extension) do + image = File.read!(file) + image_stats = ImageValidator.find_image_stats(image) + image_stats != nil && ImageStats.size_in(:megabytes, image_stats) > @max_filesize_MB + && image_stats.height <= @max_height && image_stats.width <= @max_width + && @max_aspect_ratio >= ImageStats.aspect_ratio(image_stats) >= @min_aspect_ratio + else + false + end end # Large transformation diff --git a/web/uploaders/user_photo.ex b/web/uploaders/user_photo.ex index a17cd5b80..ec073374c 100644 --- a/web/uploaders/user_photo.ex +++ b/web/uploaders/user_photo.ex @@ -1,18 +1,32 @@ defmodule CodeCorps.UserPhoto do use Arc.Definition - # Include ecto support (requires package arc_ecto installed): use Arc.Ecto.Definition + alias CodeCorps.Validators.ImageValidator + alias CodeCorps.Validators.ImageStats @versions [:original, :large, :thumb] - @acl :public_read - @icon_color_generator Application.get_env(:code_corps, :icon_color_generator) + @max_filesize_MB 16 + @max_height 10_000 + @max_width 10_000 + @max_aspect_ratio 4 + @min_aspect_ratio 0.25 + # Whitelist file extensions: def validate({file, _}) do - ~w(.jpg .jpeg .gif .png) |> Enum.member?(Path.extname(file.file_name)) + file_extension = Path.extname(file.file_name) + if ~w(.jpg .jpeg .gif .png) |> Enum.member?(file_extension) do + image = File.read!(file) + image_stats = ImageValidator.find_image_stats(image) + image_stats != nil && ImageStats.size_in(:megabytes, image_stats) > @max_filesize_MB + && image_stats.height <= @max_height && image_stats.width <= @max_width + && @max_aspect_ratio >= ImageStats.aspect_ratio(image_stats) >= @min_aspect_ratio + else + false + end end # Large transformation From 5acce78592d3f265a28a4064bc3a28406d3e76e5 Mon Sep 17 00:00:00 2001 From: Rory Quinlan Date: Wed, 26 Oct 2016 15:00:55 +0200 Subject: [PATCH 2/2] listen to credo for style --- lib/code_corps/validators/image_validator.ex | 11 +++-------- web/uploaders/organization_icon.ex | 4 ++-- web/uploaders/project_icon.ex | 4 ++-- web/uploaders/user_photo.ex | 4 ++-- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/code_corps/validators/image_validator.ex b/lib/code_corps/validators/image_validator.ex index e7e6b1910..1d1be03bd 100644 --- a/lib/code_corps/validators/image_validator.ex +++ b/lib/code_corps/validators/image_validator.ex @@ -4,8 +4,6 @@ defmodule CodeCorps.Validators.ImageStats do """ @magnitudes [:bytes, :kilobytes, :megabytes, :gigabytes, :terabytes] - @doc """ - """ defstruct [ filetype: nil, bytes: nil, @@ -48,8 +46,6 @@ defmodule CodeCorps.Validators.ImageValidator do @gif_87_signature "GIF87a" @ihdr_label "IHDR" - @doc """ - """ def find_image_stats(image_binary) do image_stats = parse_image_stats(image_binary) if image_stats == nil do @@ -95,7 +91,6 @@ defmodule CodeCorps.Validators.ImageValidator do height: height } end - # jpeg images will defp parse_image_stats(<< 0xFF, 0xD8, image_binary::binary >>) do pieces_if_baseline = String.split(image_binary, << 0xFF, 0xC0 >>) pieces_if_progressive = String.split(image_binary, << 0xFF, 0xC2 >>) @@ -103,7 +98,7 @@ defmodule CodeCorps.Validators.ImageValidator do baseline_piece_count = len_func.(pieces_if_baseline) progressive_piece_count = len_func.(pieces_if_progressive) if baseline_piece_count == progressive_piece_count == 1 do - # fail if neither baseline or progressive indicators are present + # jpeg images will fail if neither baseline or progressive indicators are present nil else # otherwise, go by the more frequent indicator @@ -123,9 +118,9 @@ defmodule CodeCorps.Validators.ImageValidator do # for jpegs, it's not easy to tell which size height and width refers to the base image # as opposed to the thumbnail(s), so we'll just go by the biggest one we find - defp parse_jpeg_pieces([ _prefix_piece | tail ]), do: parse_jpeg_pieces(tail, 0, 0, 0) + defp parse_jpeg_pieces([_prefix_piece | tail]), do: parse_jpeg_pieces(tail, 0, 0, 0) defp parse_jpeg_pieces([], height, width, _area), do: {height, width} - defp parse_jpeg_pieces([ current_piece | tail ], height, width, area) do + defp parse_jpeg_pieces([current_piece | tail], height, width, area) do << _skipped_stats::size(24), current_height::little-integer-size(16), current_width::little-integer-size(16), diff --git a/web/uploaders/organization_icon.ex b/web/uploaders/organization_icon.ex index 64594dd32..3a15a4f00 100644 --- a/web/uploaders/organization_icon.ex +++ b/web/uploaders/organization_icon.ex @@ -8,7 +8,7 @@ defmodule CodeCorps.OrganizationIcon do @versions [:original, :large, :thumb] @acl :public_read @icon_color_generator Application.get_env(:code_corps, :icon_color_generator) - @max_filesize_MB 16 + @max_filesize_mb 16 @max_height 10_000 @max_width 10_000 @max_aspect_ratio 4 @@ -20,7 +20,7 @@ defmodule CodeCorps.OrganizationIcon do if ~w(.jpg .jpeg .gif .png) |> Enum.member?(file_extension) do image = File.read!(file) image_stats = ImageValidator.find_image_stats(image) - image_stats != nil && ImageStats.size_in(:megabytes, image_stats) > @max_filesize_MB + image_stats != nil && ImageStats.size_in(:megabytes, image_stats) > @max_filesize_mb && image_stats.height <= @max_height && image_stats.width <= @max_width && @max_aspect_ratio >= ImageStats.aspect_ratio(image_stats) >= @min_aspect_ratio else diff --git a/web/uploaders/project_icon.ex b/web/uploaders/project_icon.ex index 4fee7b8fc..ec925b703 100644 --- a/web/uploaders/project_icon.ex +++ b/web/uploaders/project_icon.ex @@ -8,7 +8,7 @@ defmodule CodeCorps.ProjectIcon do @versions [:original, :large, :thumb] @acl :public_read @icon_color_generator Application.get_env(:code_corps, :icon_color_generator) - @max_filesize_MB 16 + @max_filesize_mb 16 @max_height 10_000 @max_width 10_000 @max_aspect_ratio 4 @@ -20,7 +20,7 @@ defmodule CodeCorps.ProjectIcon do if ~w(.jpg .jpeg .gif .png) |> Enum.member?(file_extension) do image = File.read!(file) image_stats = ImageValidator.find_image_stats(image) - image_stats != nil && ImageStats.size_in(:megabytes, image_stats) > @max_filesize_MB + image_stats != nil && ImageStats.size_in(:megabytes, image_stats) > @max_filesize_mb && image_stats.height <= @max_height && image_stats.width <= @max_width && @max_aspect_ratio >= ImageStats.aspect_ratio(image_stats) >= @min_aspect_ratio else diff --git a/web/uploaders/user_photo.ex b/web/uploaders/user_photo.ex index ec073374c..7a79bcea6 100644 --- a/web/uploaders/user_photo.ex +++ b/web/uploaders/user_photo.ex @@ -8,7 +8,7 @@ defmodule CodeCorps.UserPhoto do @versions [:original, :large, :thumb] @acl :public_read @icon_color_generator Application.get_env(:code_corps, :icon_color_generator) - @max_filesize_MB 16 + @max_filesize_mb 16 @max_height 10_000 @max_width 10_000 @max_aspect_ratio 4 @@ -21,7 +21,7 @@ defmodule CodeCorps.UserPhoto do if ~w(.jpg .jpeg .gif .png) |> Enum.member?(file_extension) do image = File.read!(file) image_stats = ImageValidator.find_image_stats(image) - image_stats != nil && ImageStats.size_in(:megabytes, image_stats) > @max_filesize_MB + image_stats != nil && ImageStats.size_in(:megabytes, image_stats) > @max_filesize_mb && image_stats.height <= @max_height && image_stats.width <= @max_width && @max_aspect_ratio >= ImageStats.aspect_ratio(image_stats) >= @min_aspect_ratio else