mirror of
https://codeberg.org/keyoxide/keyoxide-web.git
synced 2024-12-22 14:59:29 -07:00
Add API data sanitization
This commit is contained in:
parent
d89d970a15
commit
5b5ccbb590
4 changed files with 229 additions and 20 deletions
196
api/v0/index.js
196
api/v0/index.js
|
@ -29,17 +29,149 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
||||||
*/
|
*/
|
||||||
const router = require('express').Router()
|
const router = require('express').Router()
|
||||||
const { check, validationResult } = require('express-validator')
|
const { check, validationResult } = require('express-validator')
|
||||||
|
const Ajv = require("ajv")
|
||||||
|
const ajv = new Ajv({coerceTypes: true})
|
||||||
const kx = require('../../server')
|
const kx = require('../../server')
|
||||||
|
|
||||||
|
const apiProfileSchema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
keyData: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
fingerprint: {
|
||||||
|
type: "string"
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
userData: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
email: { type: "string" },
|
||||||
|
comment: { type: "string" },
|
||||||
|
isPrimary: { type: "boolean" },
|
||||||
|
isRevoked: { type: "boolean" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
claims: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
claimVersion: { type: "integer" },
|
||||||
|
uri: { type: "string" },
|
||||||
|
fingerprint: { type: "string" },
|
||||||
|
status: { type: "string" },
|
||||||
|
matches: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
serviceProvider: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
type: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
match: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
regularExpression: { type: "object" },
|
||||||
|
isAmbiguous: { type: "boolean" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
display: { type: "string" },
|
||||||
|
uri: { type: "string" },
|
||||||
|
qr: { type: "string" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
proof: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
uri: { type: "string" },
|
||||||
|
request: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
fetcher: { type: "string" },
|
||||||
|
access: { type: "string" },
|
||||||
|
format: { type: "string" },
|
||||||
|
data: { type: "object" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
claim: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
format: { type: "string" },
|
||||||
|
relation: { type: "string" },
|
||||||
|
path: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verification: {
|
||||||
|
type: "object"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
primaryUserIndex: {
|
||||||
|
type: "integer"
|
||||||
|
},
|
||||||
|
key: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
data: { type: "object" },
|
||||||
|
fetchMethod: { type: "string" },
|
||||||
|
uri: { type: "string" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
avatarURL: { type: "string" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
type: "array"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["keyData", "extra", "errors"],
|
||||||
|
additionalProperties: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiProfileValidate = ajv.compile(apiProfileSchema)
|
||||||
|
|
||||||
const doVerification = async (data) => {
|
const doVerification = async (data) => {
|
||||||
let promises = []
|
let promises = []
|
||||||
let results = []
|
let results = []
|
||||||
|
|
||||||
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
|
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
|
||||||
const user = data.keyData.users[iUser];
|
const user = data.keyData.users[iUser]
|
||||||
|
|
||||||
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
||||||
const claim = user.claims[iClaim];
|
const claim = user.claims[iClaim]
|
||||||
|
|
||||||
promises.push(
|
promises.push(
|
||||||
new Promise(async (resolve, reject) => {
|
new Promise(async (resolve, reject) => {
|
||||||
|
@ -59,6 +191,34 @@ const doVerification = async (data) => {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sanitize = (data) => {
|
||||||
|
let results = []
|
||||||
|
|
||||||
|
const dataClone = JSON.parse(JSON.stringify(data))
|
||||||
|
|
||||||
|
for (let iUser = 0; iUser < dataClone.keyData.users.length; iUser++) {
|
||||||
|
const user = dataClone.keyData.users[iUser]
|
||||||
|
|
||||||
|
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
||||||
|
const claim = user.claims[iClaim]
|
||||||
|
|
||||||
|
// TODO Fix upstream
|
||||||
|
if (!claim.verification) {
|
||||||
|
claim.verification = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.keyData.users[iUser].claims[iClaim] = claim
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = apiProfileValidate(data)
|
||||||
|
if (!valid) {
|
||||||
|
throw new Error(`Profile data sanitization error`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
router.get('/profile/fetch',
|
router.get('/profile/fetch',
|
||||||
check('query').exists(),
|
check('query').exists(),
|
||||||
check('protocol').optional().toLowerCase().isIn(["hkp", "wkd"]),
|
check('protocol').optional().toLowerCase().isIn(["hkp", "wkd"]),
|
||||||
|
@ -100,7 +260,21 @@ router.get('/profile/fetch',
|
||||||
data = await doVerification(data)
|
data = await doVerification(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send(data)
|
try {
|
||||||
|
// Sanitize JSON
|
||||||
|
data = sanitize(data);
|
||||||
|
} catch (error) {
|
||||||
|
data.keyData = {}
|
||||||
|
data.extra = {}
|
||||||
|
data.errors = [error.message]
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusCode = 200
|
||||||
|
if (data.errors.length > 0) {
|
||||||
|
statusCode = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).send(data)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -116,7 +290,21 @@ router.get('/profile/verify',
|
||||||
// Do verification
|
// Do verification
|
||||||
data = await doVerification(req.query.data)
|
data = await doVerification(req.query.data)
|
||||||
|
|
||||||
res.send(data)
|
try {
|
||||||
|
// Sanitize JSON
|
||||||
|
data = sanitize(data);
|
||||||
|
} catch (error) {
|
||||||
|
data.keyData = {}
|
||||||
|
data.extra = {}
|
||||||
|
data.errors = [error.message]
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusCode = 200
|
||||||
|
if (data.errors.length > 0) {
|
||||||
|
statusCode = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).send(data)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"description": "A modern, secure and privacy-friendly platform to establish your decentralized online identity",
|
"description": "A modern, secure and privacy-friendly platform to establish your decentralized online identity",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ajv": "^8.6.3",
|
||||||
"bent": "^7.3.12",
|
"bent": "^7.3.12",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"dialog-polyfill": "^0.5.6",
|
"dialog-polyfill": "^0.5.6",
|
||||||
|
|
|
@ -37,7 +37,7 @@ const generateWKDProfile = async (id) => {
|
||||||
let keyData = await doip.keys.process(key.publicKey)
|
let keyData = await doip.keys.process(key.publicKey)
|
||||||
keyData.key.fetchMethod = 'wkd'
|
keyData.key.fetchMethod = 'wkd'
|
||||||
keyData.key.uri = key.fetchURL
|
keyData.key.uri = key.fetchURL
|
||||||
keyData.key.data = null
|
keyData.key.data = {}
|
||||||
keyData = processKeyData(keyData)
|
keyData = processKeyData(keyData)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -49,9 +49,9 @@ const generateWKDProfile = async (id) => {
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
return {
|
return {
|
||||||
key: null,
|
key: {},
|
||||||
keyData: null,
|
keyData: {},
|
||||||
extra: null,
|
extra: {},
|
||||||
errors: [err.message]
|
errors: [err.message]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -63,7 +63,7 @@ const generateHKPProfile = async (id, keyserverDomain) => {
|
||||||
let keyData = await doip.keys.process(key.publicKey)
|
let keyData = await doip.keys.process(key.publicKey)
|
||||||
keyData.key.fetchMethod = 'hkp'
|
keyData.key.fetchMethod = 'hkp'
|
||||||
keyData.key.uri = key.fetchURL
|
keyData.key.uri = key.fetchURL
|
||||||
keyData.key.data = null
|
keyData.key.data = {}
|
||||||
keyData = processKeyData(keyData)
|
keyData = processKeyData(keyData)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -75,9 +75,9 @@ const generateHKPProfile = async (id, keyserverDomain) => {
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
return {
|
return {
|
||||||
key: null,
|
key: {},
|
||||||
keyData: null,
|
keyData: {},
|
||||||
extra: null,
|
extra: {},
|
||||||
errors: [err.message]
|
errors: [err.message]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -88,7 +88,7 @@ const generateSignatureProfile = async (signature) => {
|
||||||
.then(async key => {
|
.then(async key => {
|
||||||
let keyData = key.keyData
|
let keyData = key.keyData
|
||||||
delete key.keyData
|
delete key.keyData
|
||||||
keyData.key.data = null
|
keyData.key.data = {}
|
||||||
keyData = processKeyData(keyData)
|
keyData = processKeyData(keyData)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -100,9 +100,9 @@ const generateSignatureProfile = async (signature) => {
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
return {
|
return {
|
||||||
key: null,
|
key: {},
|
||||||
keyData: null,
|
keyData: {},
|
||||||
extra: null,
|
extra: {},
|
||||||
errors: [err.message]
|
errors: [err.message]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -114,7 +114,7 @@ const generateKeybaseProfile = async (username, fingerprint) => {
|
||||||
let keyData = await doip.keys.process(key.publicKey)
|
let keyData = await doip.keys.process(key.publicKey)
|
||||||
keyData.key.fetchMethod = 'hkp'
|
keyData.key.fetchMethod = 'hkp'
|
||||||
keyData.key.uri = key.fetchURL
|
keyData.key.uri = key.fetchURL
|
||||||
keyData.key.data = null
|
keyData.key.data = {}
|
||||||
keyData = processKeyData(keyData)
|
keyData = processKeyData(keyData)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -126,9 +126,9 @@ const generateKeybaseProfile = async (username, fingerprint) => {
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
return {
|
return {
|
||||||
key: null,
|
key: {},
|
||||||
keyData: null,
|
keyData: {},
|
||||||
extra: null,
|
extra: {},
|
||||||
errors: [err.message]
|
errors: [err.message]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
20
yarn.lock
20
yarn.lock
|
@ -645,6 +645,16 @@ ajv@^6.12.3:
|
||||||
json-schema-traverse "^0.4.1"
|
json-schema-traverse "^0.4.1"
|
||||||
uri-js "^4.2.2"
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
|
ajv@^8.6.3:
|
||||||
|
version "8.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764"
|
||||||
|
integrity sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal "^3.1.1"
|
||||||
|
json-schema-traverse "^1.0.0"
|
||||||
|
require-from-string "^2.0.2"
|
||||||
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
ansi-align@^3.0.0:
|
ansi-align@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz"
|
resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz"
|
||||||
|
@ -2666,6 +2676,11 @@ json-schema-traverse@^0.4.1:
|
||||||
resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
|
resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
|
||||||
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
||||||
|
|
||||||
|
json-schema-traverse@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
|
||||||
|
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||||
|
|
||||||
json-schema@0.2.3:
|
json-schema@0.2.3:
|
||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz"
|
resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz"
|
||||||
|
@ -3706,6 +3721,11 @@ require-directory@^2.1.1:
|
||||||
resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
|
resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
|
||||||
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
|
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
|
||||||
|
|
||||||
|
require-from-string@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||||
|
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||||
|
|
||||||
require-main-filename@^2.0.0:
|
require-main-filename@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz"
|
||||||
|
|
Loading…
Reference in a new issue