///|
pub(all) enum UploadRisk {
LowRisk
MediumRisk
HighRisk
} derive(Debug, Eq)
///|
pub(all) enum BodySizeClass {
EmptyBody
TinyBody
SmallBody
MediumBody
LargeBody
} derive(Debug, Eq)
///|
pub(all) struct NameCount {
name : String
count : Int
} derive(Debug, Eq)
///|
pub(all) struct FieldProfile {
name : String
values : Int
total_length : Int
max_length : Int
empty_values : Int
duplicated : Bool
size_class : BodySizeClass
} derive(Debug, Eq)
///|
pub(all) struct FileProfile {
name : String
filename : String
content_type : String
length : Int
empty : Bool
extension : String?
risk : UploadRisk
notes : Array[String]
size_class : BodySizeClass
} derive(Debug, Eq)
///|
pub(all) struct HeaderProfile {
name : String
count : Int
first_value : String
} derive(Debug, Eq)
///|
pub(all) struct FormAnalysis {
summary : FormSummary
fields : Array[FieldProfile]
files : Array[FileProfile]
content_types : Array[NameCount]
headers : Array[HeaderProfile]
risk : UploadRisk
notes : Array[String]
} derive(Debug, Eq)
///|
pub fn analyze_form(form : MultipartForm) -> FormAnalysis {
let fields = analyze_fields(form)
let files = analyze_files(form)
let content_types = analyze_content_types(form)
let headers = analyze_headers(form)
let notes = analysis_notes(form, fields, files)
let risk = analysis_risk(form, fields, files, notes)
{
summary: form.summary(),
fields,
files,
content_types,
headers,
risk,
notes,
}
}
///|
pub fn analyze_request(
request : MultipartRequest,
) -> Result[FormAnalysis, MultipartError] {
match roundtrip_request(request) {
Ok(form) => Ok(analyze_form(form))
Err(err) => Err(err)
}
}
///|
pub fn request_analysis_lines(
request : MultipartRequest,
) -> Result[Array[String], MultipartError] {
match analyze_request(request) {
Ok(analysis) => Ok(analysis.to_lines())
Err(err) => Err(err)
}
}
///|
pub fn FormAnalysis::compact_line(self : FormAnalysis) -> String {
self.summary.to_line() +
", risk=" +
self.risk.label() +
", notes=" +
self.notes.length().to_string()
}
///|
pub fn FormAnalysis::to_lines(self : FormAnalysis) -> Array[String] {
let lines = Array::new()
lines.push("MoonFormData analysis")
lines.push(self.compact_line())
lines.push("fields=" + self.fields.length().to_string())
let mut i = 0
while i < self.fields.length() {
lines.push("field " + self.fields[i].to_line())
i = i + 1
}
lines.push("files=" + self.files.length().to_string())
let mut j = 0
while j < self.files.length() {
lines.push("file " + self.files[j].to_line())
j = j + 1
}
if self.content_types.length() > 0 {
lines.push("content-types=" + format_name_counts(self.content_types))
}
if self.headers.length() > 0 {
lines.push("headers=" + self.headers.length().to_string())
}
let mut n = 0
while n < self.notes.length() {
lines.push("note " + self.notes[n])
n = n + 1
}
lines
}
///|
pub fn FormAnalysis::has_note(self : FormAnalysis, needle : String) -> Bool {
let mut i = 0
while i < self.notes.length() {
if self.notes[i].find(needle) is Some(_) {
return true
}
i = i + 1
}
false
}
///|
pub fn FormAnalysis::is_low_risk(self : FormAnalysis) -> Bool {
self.risk == LowRisk
}
///|
pub fn FormAnalysis::is_high_risk(self : FormAnalysis) -> Bool {
self.risk == HighRisk
}
///|
pub fn FieldProfile::to_line(self : FieldProfile) -> String {
self.name +
": values=" +
self.values.to_string() +
", total=" +
self.total_length.to_string() +
", max=" +
self.max_length.to_string() +
", empty=" +
self.empty_values.to_string() +
", duplicate=" +
self.duplicated.to_string() +
", size=" +
self.size_class.label()
}
///|
pub fn FileProfile::to_line(self : FileProfile) -> String {
self.name +
": filename=" +
self.filename +
", type=" +
self.content_type +
", bytes=" +
self.length.to_string() +
", empty=" +
self.empty.to_string() +
", risk=" +
self.risk.label() +
", size=" +
self.size_class.label()
}
///|
pub fn HeaderProfile::to_line(self : HeaderProfile) -> String {
self.name +
": count=" +
self.count.to_string() +
", first=" +
self.first_value
}
///|
pub fn UploadRisk::label(self : UploadRisk) -> String {
match self {
LowRisk => "low"
MediumRisk => "medium"
HighRisk => "high"
}
}
///|
pub fn BodySizeClass::label(self : BodySizeClass) -> String {
match self {
EmptyBody => "empty"
TinyBody => "tiny"
SmallBody => "small"
MediumBody => "medium"
LargeBody => "large"
}
}
///|
pub fn body_size_class(length : Int) -> BodySizeClass {
if length <= 0 {
EmptyBody
} else if length <= 1024 {
TinyBody
} else if length <= 1024 * 64 {
SmallBody
} else if length <= 1024 * 1024 {
MediumBody
} else {
LargeBody
}
}
///|
pub fn count_by_name(values : Array[String]) -> Array[NameCount] {
let counts = Array::new()
let mut i = 0
while i < values.length() {
increment_name_count(counts, values[i])
i = i + 1
}
counts
}
///|
pub fn find_name_count(counts : Array[NameCount], name : String) -> Int {
let mut i = 0
while i < counts.length() {
if counts[i].name == name {
return counts[i].count
}
i = i + 1
}
0
}
///|
pub fn form_name_counts(form : MultipartForm) -> Array[NameCount] {
let names = Array::new()
let mut i = 0
while i < form.parts.length() {
names.push(form.parts[i].name)
i = i + 1
}
count_by_name(names)
}
///|
pub fn form_content_type_counts(form : MultipartForm) -> Array[NameCount] {
analyze_content_types(form)
}
///|
pub fn detect_boundary_in_part_bodies(form : MultipartForm) -> Bool {
let marker = "--" + form.boundary
let mut i = 0
while i < form.parts.length() {
if form.parts[i].body.find(marker) is Some(_) {
return true
}
i = i + 1
}
false
}
///|
fn analyze_fields(form : MultipartForm) -> Array[FieldProfile] {
let names = form.field_names()
let fields = Array::new()
let mut i = 0
while i < names.length() {
fields.push(field_profile(form, names[i]))
i = i + 1
}
fields
}
///|
fn field_profile(form : MultipartForm, name : String) -> FieldProfile {
let values = form.field_values(name)
let mut total = 0
let mut max = 0
let mut empty = 0
let mut i = 0
while i < values.length() {
let len = values[i].length()
total = total + len
if len > max {
max = len
}
if len == 0 {
empty = empty + 1
}
i = i + 1
}
{
name,
values: values.length(),
total_length: total,
max_length: max,
empty_values: empty,
duplicated: values.length() > 1,
size_class: body_size_class(max),
}
}
///|
fn analyze_files(form : MultipartForm) -> Array[FileProfile] {
let profiles = Array::new()
let files = form.file_parts()
let mut i = 0
while i < files.length() {
profiles.push(file_profile(files[i]))
i = i + 1
}
profiles
}
///|
fn file_profile(part : FormPart) -> FileProfile {
let filename = part.filename.unwrap_or("upload.bin")
let content_type = part.content_type.unwrap_or("application/octet-stream")
let notes = filename_security_notes(filename)
let extension = filename_extension(filename)
let risk = file_risk(part, filename, content_type, notes)
{
name: part.name,
filename,
content_type,
length: part.body.length(),
empty: part.body.length() == 0,
extension,
risk,
notes,
size_class: body_size_class(part.body.length()),
}
}
///|
fn file_risk(
part : FormPart,
filename : String,
content_type : String,
notes : Array[String],
) -> UploadRisk {
let ext = filename_extension(filename).unwrap_or("")
if notes.length() > 0 ||
is_known_executable_extension(ext) ||
content_type == "application/x-msdownload" {
HighRisk
} else if part.body.length() == 0 ||
content_type == "application/octet-stream" ||
ext == "" {
MediumRisk
} else {
LowRisk
}
}
///|
fn is_known_executable_extension(ext : String) -> Bool {
ext == "exe" ||
ext == "bat" ||
ext == "cmd" ||
ext == "com" ||
ext == "scr" ||
ext == "ps1" ||
ext == "sh" ||
ext == "dll" ||
ext == "so" ||
ext == "js"
}
///|
fn analyze_content_types(form : MultipartForm) -> Array[NameCount] {
let values = Array::new()
let files = form.file_parts()
let mut i = 0
while i < files.length() {
values.push(files[i].content_type.unwrap_or("application/octet-stream"))
i = i + 1
}
count_by_name(values)
}
///|
fn analyze_headers(form : MultipartForm) -> Array[HeaderProfile] {
let profiles = Array::new()
let mut p = 0
while p < form.parts.length() {
let headers = form.parts[p].headers
let mut h = 0
while h < headers.length() {
let (name, value) = headers[h]
increment_header_profile(profiles, normalize_header_name(name), value)
h = h + 1
}
p = p + 1
}
profiles
}
///|
fn analysis_notes(
form : MultipartForm,
fields : Array[FieldProfile],
files : Array[FileProfile],
) -> Array[String] {
let notes = Array::new()
if form.parts.length() == 0 {
notes.push("form has no parts")
}
if detect_boundary_in_part_bodies(form) {
notes.push("part body contains the active boundary marker")
}
let mut i = 0
while i < fields.length() {
if fields[i].duplicated {
notes.push("duplicate text field: " + fields[i].name)
}
if fields[i].empty_values > 0 {
notes.push("empty text field value: " + fields[i].name)
}
i = i + 1
}
let mut j = 0
while j < files.length() {
if files[j].empty {
notes.push("empty file: " + files[j].name)
}
let mut n = 0
while n < files[j].notes.length() {
notes.push("filename " + files[j].notes[n] + ": " + files[j].filename)
n = n + 1
}
if files[j].risk == HighRisk {
notes.push("high risk file: " + files[j].filename)
}
j = j + 1
}
notes
}
///|
fn analysis_risk(
form : MultipartForm,
fields : Array[FieldProfile],
files : Array[FileProfile],
notes : Array[String],
) -> UploadRisk {
let mut risk = LowRisk
if form.parts.length() == 0 || notes.length() > 0 {
risk = max_risk(risk, MediumRisk)
}
if detect_boundary_in_part_bodies(form) {
risk = max_risk(risk, HighRisk)
}
let mut i = 0
while i < fields.length() {
if fields[i].size_class == LargeBody {
risk = max_risk(risk, HighRisk)
} else if fields[i].size_class == MediumBody {
risk = max_risk(risk, MediumRisk)
}
i = i + 1
}
let mut j = 0
while j < files.length() {
risk = max_risk(risk, files[j].risk)
if files[j].size_class == LargeBody {
risk = max_risk(risk, HighRisk)
}
j = j + 1
}
risk
}
///|
fn max_risk(a : UploadRisk, b : UploadRisk) -> UploadRisk {
if risk_rank(a) >= risk_rank(b) {
a
} else {
b
}
}
///|
fn risk_rank(risk : UploadRisk) -> Int {
match risk {
LowRisk => 1
MediumRisk => 2
HighRisk => 3
}
}
///|
fn increment_name_count(counts : Array[NameCount], name : String) -> Unit {
let mut i = 0
while i < counts.length() {
if counts[i].name == name {
let current = counts[i]
counts[i] = { name: current.name, count: current.count + 1 }
return
}
i = i + 1
}
counts.push({ name, count: 1 })
}
///|
fn increment_header_profile(
profiles : Array[HeaderProfile],
name : String,
value : String,
) -> Unit {
let mut i = 0
while i < profiles.length() {
if profiles[i].name.compare_ignore_ascii_case(name) == 0 {
let current = profiles[i]
profiles[i] = {
name: current.name,
count: current.count + 1,
first_value: current.first_value,
}
return
}
i = i + 1
}
profiles.push({ name, count: 1, first_value: value })
}
///|
fn format_name_counts(counts : Array[NameCount]) -> String {
let pieces = Array::new()
let mut i = 0
while i < counts.length() {
pieces.push(counts[i].name + "=" + counts[i].count.to_string())
i = i + 1
}
pieces.join(", ")
}