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:
parent
9fd09a8dfb
commit
70df29790a
5 changed files with 53 additions and 13 deletions
13
lib/migrations/20180525153000-user-add-delete-token.js
Normal file
13
lib/migrations/20180525153000-user-add-delete-token.js
Normal 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')
|
||||||
|
}
|
||||||
|
}
|
|
@ -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: {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
user.destroy().then(function () {
|
return response.errorNotFound(res)
|
||||||
res.redirect(config.serverURL + '/')
|
}
|
||||||
})
|
if (user.deleteToken === req.params.token) {
|
||||||
|
user.destroy().then(function () {
|
||||||
|
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'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue