Additional validation and escaping for file names.

This adds additional front and backend validations for task order file
names. We are now restricting file names to a whitelist regex of
[A-Za-z0-9\-_ \.] for simplicity.

Note:
On the frontend, the filename string must have at least one character.
This is not true in the backend validation; because of the way the
entire task order form is validated, requiring input would break the
business logic currently implemented.
This commit is contained in:
dandds 2020-01-12 11:33:33 -05:00
parent 05bc8c3819
commit 5213657b0f
7 changed files with 35 additions and 5 deletions

View File

@ -7,7 +7,7 @@ from wtforms.fields import (
HiddenField, HiddenField,
) )
from wtforms.fields.html5 import DateField from wtforms.fields.html5 import DateField
from wtforms.validators import Required, Length, NumberRange, ValidationError from wtforms.validators import Required, Length, NumberRange, ValidationError, Regexp
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from numbers import Number from numbers import Number
@ -15,6 +15,7 @@ from .data import JEDI_CLIN_TYPES
from .fields import SelectField from .fields import SelectField
from .forms import BaseForm, remove_empty_string from .forms import BaseForm, remove_empty_string
from atst.utils.localization import translate from atst.utils.localization import translate
from .validators import REGEX_ALPHA_NUMERIC
from flask import current_app as app from flask import current_app as app
MAX_CLIN_AMOUNT = 1000000000 MAX_CLIN_AMOUNT = 1000000000
@ -116,7 +117,10 @@ class AttachmentForm(BaseForm):
filename = HiddenField( filename = HiddenField(
id="attachment_filename", id="attachment_filename",
validators=[ validators=[
Length(max=100, message=translate("forms.attachment.filename.length_error")) Length(
max=100, message=translate("forms.attachment.filename.length_error")
),
Regexp(regex=REGEX_ALPHA_NUMERIC),
], ],
) )
object_name = HiddenField( object_name = HiddenField(
@ -124,7 +128,8 @@ class AttachmentForm(BaseForm):
validators=[ validators=[
Length( Length(
max=40, message=translate("forms.attachment.object_name.length_error") max=40, message=translate("forms.attachment.object_name.length_error")
) ),
Regexp(regex=REGEX_ALPHA_NUMERIC),
], ],
) )
accept = ".pdf,application/pdf" accept = ".pdf,application/pdf"

View File

@ -8,6 +8,9 @@ import pendulum
from atst.utils.localization import translate from atst.utils.localization import translate
REGEX_ALPHA_NUMERIC = "^[A-Za-z0-9\-_ \.]*$"
def DateRange(lower_bound=None, upper_bound=None, message=None): def DateRange(lower_bound=None, upper_bound=None, message=None):
def _date_range(form, field): def _date_range(form, field):
if field.data is None: if field.data is None:

View File

@ -70,7 +70,7 @@ describe('UploadInput Test', () => {
}) })
const component = wrapper.find(uploadinput) const component = wrapper.find(uploadinput)
const event = { target: { value: '', files: [{ name: '' }] } } const event = { target: { value: '', files: [{ name: 'sample.pdf' }] } }
component.setMethods({ component.setMethods({
getUploader: async () => new MockUploader('token', 'objectName'), getUploader: async () => new MockUploader('token', 'objectName'),

View File

@ -1,5 +1,6 @@
import { buildUploader } from '../lib/upload' import { buildUploader } from '../lib/upload'
import { emitFieldChange } from '../lib/emitters' import { emitFieldChange } from '../lib/emitters'
import inputValidations from '../lib/input_validations'
export default { export default {
name: 'uploadinput', name: 'uploadinput',
@ -28,6 +29,7 @@ export default {
changed: false, changed: false,
uploadError: false, uploadError: false,
sizeError: false, sizeError: false,
filenameError: false,
downloadLink: '', downloadLink: '',
} }
}, },
@ -50,6 +52,10 @@ export default {
this.sizeError = true this.sizeError = true
return return
} }
if (!this.validateFileName(file.name)) {
this.filenameError = true
return
}
const uploader = await this.getUploader() const uploader = await this.getUploader()
const response = await uploader.upload(file) const response = await uploader.upload(file)
@ -71,6 +77,10 @@ export default {
this.uploadError = true this.uploadError = true
} }
}, },
validateFileName: function(name) {
const regex = inputValidations.restrictedFileName.match
return regex.test(name)
},
removeAttachment: function(e) { removeAttachment: function(e) {
e.preventDefault() e.preventDefault()
this.attachment = null this.attachment = null
@ -118,7 +128,8 @@ export default {
return ( return (
(!this.changed && this.initialErrors) || (!this.changed && this.initialErrors) ||
this.uploadError || this.uploadError ||
this.sizeError this.sizeError ||
this.filenameError
) )
}, },
valid: function() { valid: function() {

View File

@ -104,4 +104,11 @@ export default {
unmask: ['(', ')', '-', ' '], unmask: ['(', ')', '-', ' '],
validationError: 'Please enter a 10-digit phone number', validationError: 'Please enter a 10-digit phone number',
}, },
restrictedFileName: {
mask: false,
match: /^[A-Za-z0-9\-_ \.]+$/,
unmask: [],
validationError:
'File names can only contain the characters A-Z, 0-9, space, hyphen, underscore, and period.',
},
} }

View File

@ -49,6 +49,9 @@
<template v-if="sizeError"> <template v-if="sizeError">
<span class="usa-input__message">{{ "forms.task_order.size_error" | translate }}</span> <span class="usa-input__message">{{ "forms.task_order.size_error" | translate }}</span>
</template> </template>
<template v-if="filenameError">
<span class="usa-input__message">{{ "forms.task_order.filename_error" | translate }}</span>
</template>
{% for error, error_messages in field.errors.items() %} {% for error, error_messages in field.errors.items() %}
<span class="usa-input__message">{{error_messages[0]}}</span> <span class="usa-input__message">{{error_messages[0]}}</span>
{% endfor %} {% endfor %}

View File

@ -292,6 +292,7 @@ forms:
task_order: task_order:
upload_error: There was an error uploading your file. Please try again. If you encounter repeated problems uploading this file, please contact CCPO. upload_error: There was an error uploading your file. Please try again. If you encounter repeated problems uploading this file, please contact CCPO.
size_error: The file you have selected is too large. Please choose a file no larger than 64MB. size_error: The file you have selected is too large. Please choose a file no larger than 64MB.
filename_error: File names can only contain the characters A-Z, 0-9, space, hyphen, underscore, and period.
defense_component_label: Select DoD component(s) funding your Portfolio defense_component_label: Select DoD component(s) funding your Portfolio
file_format_not_allowed: Only PDF or PNG files can be uploaded. file_format_not_allowed: Only PDF or PNG files can be uploaded.
number_description: Task order number (13 digits) number_description: Task order number (13 digits)