Add token based security feature

In the current setup users could be tricked into deleting their data by
providing a malicious link like `[click me](/me/delete)`. This commit
prevents such an easy attack and need the user's deleteToken to get his
data deleted. In case someone requests his deletion by email you can
also ask him for this token.

We can add a GUI that shows it later on.

Signed-off-by: Sheogorath <sheogorath@shivering-isles.com>
This commit is contained in:
Sheogorath 2018-05-25 18:19:31 +02:00
parent 9fd09a8dfb
commit 70df29790a
No known key found for this signature in database
GPG key ID: 1F05CC3635CDDFFD
5 changed files with 53 additions and 13 deletions

View file

@ -0,0 +1,13 @@
'use strict'
module.exports = {
up: function (queryInterface, Sequelize) {
return queryInterface.addColumn('Users', 'deleteToken', {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4
})
},
down: function (queryInterface, Sequelize) {
return queryInterface.removeColumn('Users', 'deleteToken')
}
}

View file

@ -31,6 +31,10 @@ module.exports = function (sequelize, DataTypes) {
refreshToken: { refreshToken: {
type: DataTypes.STRING type: DataTypes.STRING
}, },
deleteToken: {
type: DataTypes.UUID,
defaultValue: Sequelize.UUIDV4
},
email: { email: {
type: Sequelize.TEXT, type: Sequelize.TEXT,
validate: { validate: {

View file

@ -56,7 +56,10 @@ function responseError (res, code, detail, msg) {
} }
function showIndex (req, res, next) { function showIndex (req, res, next) {
res.render(config.indexPath, { var authStatus = req.isAuthenticated()
var deleteToken = ''
var data = {
url: config.serverURL, url: config.serverURL,
useCDN: config.useCDN, useCDN: config.useCDN,
allowAnonymous: config.allowAnonymous, allowAnonymous: config.allowAnonymous,
@ -74,12 +77,28 @@ function showIndex (req, res, next) {
email: config.isEmailEnable, email: config.isEmailEnable,
allowEmailRegister: config.allowEmailRegister, allowEmailRegister: config.allowEmailRegister,
allowPDFExport: config.allowPDFExport, allowPDFExport: config.allowPDFExport,
signin: req.isAuthenticated(), signin: authStatus,
infoMessage: req.flash('info'), infoMessage: req.flash('info'),
errorMessage: req.flash('error'), errorMessage: req.flash('error'),
privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')), privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')),
termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')) termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')),
deleteToken: deleteToken
}
if (authStatus) {
models.User.findOne({
where: {
id: req.user.id
}
}).then(function (user) {
if (user) {
data.deleteToken = user.deleteToken
res.render(config.indexPath, data)
}
}) })
} else {
res.render(config.indexPath, data)
}
} }
function responseHackMD (res, note) { function responseHackMD (res, note) {

View file

@ -38,25 +38,29 @@ UserRouter.get('/me', function (req, res) {
}) })
// delete the currently authenticated user // delete the currently authenticated user
UserRouter.get('/me/delete', function (req, res) { UserRouter.get('/me/delete/:token?', function (req, res) {
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
models.User.findOne({ models.User.findOne({
where: { where: {
id: req.user.id id: req.user.id
} }
}).then(function (user) { }).then(function (user) {
if (!user) { return response.errorNotFound(res) } if (!user) {
return response.errorNotFound(res)
}
if (user.deleteToken === req.params.token) {
user.destroy().then(function () { user.destroy().then(function () {
res.redirect(config.serverURL + '/') res.redirect(config.serverURL + '/')
}) })
} else {
return response.errorForbidden(res)
}
}).catch(function (err) { }).catch(function (err) {
logger.error('delete user failed: ' + err) logger.error('delete user failed: ' + err)
return response.errorInternalError(res) return response.errorInternalError(res)
}) })
} else { } else {
res.send({ return response.errorForbidden(res)
status: 'forbidden'
})
} }
}) })

View file

@ -193,7 +193,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default ui-delete-user-modal-cancel" data-dismiss="modal"><%= __('Cancel') %></button> <button type="button" class="btn btn-default ui-delete-user-modal-cancel" data-dismiss="modal"><%= __('Cancel') %></button>
<a type="button" class="btn btn-danger" href="<%- url %>/me/delete"><%= __('Yes, do it!') %></a> <a type="button" class="btn btn-danger" href="<%- url %>/me/delete/<%- deleteToken %>"><%= __('Yes, do it!') %></a>
</div> </div>
</div> </div>
</div> </div>