Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly sanitize avatars in upload #1514

Merged
merged 2 commits into from
Mar 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"stylelint:fix": "stylelint src --fix"
},
"dependencies": {
"@mattkrick/sanitize-svg": "^0.2.1",
"@nextcloud/auth": "^1.2.1",
"@nextcloud/dialogs": "^1.2.1",
"@nextcloud/initial-state": "^1.1.0",
Expand Down
86 changes: 79 additions & 7 deletions src/components/ContactDetails/ContactDetailsAvatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import { ActionLink, ActionButton } from '@nextcloud/vue'
import { getFilePickerBuilder } from '@nextcloud/dialogs'
import { generateRemoteUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import sanitizeSVG from '@mattkrick/sanitize-svg'

const axios = () => import('axios')

Expand Down Expand Up @@ -147,22 +148,92 @@ export default {
if (file && file.size && file.size <= 1 * 1024 * 1024) {
const reader = new FileReader()
const self = this
let type = ''

reader.onload = function(e) {
// only getting the raw binary base64
self.setPhoto(reader.result.split(',').pop(), file.type)
reader.onloadend = async function(e) {
try {
// We got an ArrayBuffer, checking the true mime type...
if (typeof e.target.result === 'object') {
const uint = new Uint8Array(e.target.result)
const bytes = []
uint.forEach((byte) => {
bytes.push(byte.toString(16))
})
const hex = bytes.join('').toUpperCase()

if (self.getMimetype(hex).startsWith('image/')) {
type = self.getMimetype(hex)
// we got a valid image, read it again as base64
reader.readAsDataURL(file)
return
}
throw new Error('Wrong image mimetype')
}

// else we got the base64 and we're good to go!
const imageBase64 = e.target.result.split(',').pop()

if (e.target.result.indexOf('image/svg') > -1) {
const imageSvg = atob(imageBase64)
const cleanSvg = await sanitizeSVG(imageSvg)
if (!cleanSvg) {
throw new Error('Unsafe svg image')
}
}

// All is well! Set the photo
self.setPhoto(imageBase64, type)
} catch (error) {
console.error(error)
OC.Notification.showTemporary(t('contacts', 'Invalid image'))
} finally {
self.resetPicker()
}
}

reader.readAsDataURL(file)
// start by reading the magic bytes to detect proper photo mimetype
const blob = file.slice(0, 4)
reader.readAsArrayBuffer(blob)
} else {
OC.Notification.showTemporary(t('contacts', 'Image is too big (max 1MB).'))
// reset input
event.target.value = ''
this.loading = false
this.resetPicker()
}
}
},

/**
* Reset image pciker input
*/
resetPicker() {
// reset input
this.$refs.uploadInput.value = ''
this.loading = false
},

/**
* Return the mimetype based on the first magix byte
*
* @param {string} signature the first 4 bytes
* @returns {string} the mimetype
*/
getMimetype(signature) {
switch (signature) {
case '89504E47':
return 'image/png'
case '47494638':
return 'image/gif'
case '3C3F786D':
case '3C737667':
return 'image/svg+xml'
case 'FFD8FFDB':
case 'FFD8FFE0':
case 'FFD8FFE1':
return 'image/jpeg'
default:
return 'application/octet-stream'
}
},

/**
* Update the contact photo
*
Expand Down Expand Up @@ -269,6 +340,7 @@ export default {
this.updateHeightWidth(this.$refs.img.naturalHeight, this.$refs.img.naturalWidth)
}
},

/**
* Updates the current height and width data
* based on the viewer maximum size
Expand Down