diff --git a/apxtri/apxtri.js b/apxtri/apxtri.js index e78ccd9..0414cb1 100755 --- a/apxtri/apxtri.js +++ b/apxtri/apxtri.js @@ -43,6 +43,14 @@ SEE README.md to start ********************************************/ const apxtri = {}; +apxtri.istribe = (tribe) => { + if (typeof tribe !== "string" || !/^[a-z0-9_-]{1,64}$/.test(tribe)) { + return false; + } + if (tribe === "adminapi") return true; + return fs.existsSync(`../${tribe}/apxtri/routes`); +}; + apxtri.main = async () => { if (process.env.NODE_MODE !== 'dev' && !fs.existsSync("/etc/nginx/nginx.conf")) { console.log( @@ -51,39 +59,97 @@ apxtri.main = async () => { process.exit(0); } //Check prerequest data - if ( - fs.existsSync("../adminapi/objects/tribes/idx/tribes_dns.json") && - fs.existsSync("../adminapi/objects/tribes/itm/adminapi.json") - ) { - // check all tribes are in tribes_dns.json - const conf = fs.readJSONSync( - "../adminapi/objects/tribes/itm/adminapi.json" + const adminapipath = "../adminapi/objects/tribes/itm/adminapi.json"; + const tribednspath = "../adminapi/objects/tribes/idx/tribes_dns.json"; + // An existing adminapi.json means this is not a first install: never + // overwrite it from the template, even if tribes_dns.json is missing. + if (fs.existsSync(adminapipath)) { + // check all tribes are in tribes_dns.json (rebuild it if needed) + const conf = fs.readJSONSync(adminapipath); + const tribesdns = fs.existsSync(tribednspath) + ? fs.readJsonSync(tribednspath) + : {}; + Object.keys(tribesdns).forEach((tribe) => { + if (!apxtri.istribe(tribe)) { + console.log( + `\x1b[31m drop unknown tribe from tribes_dns.json: ${tribe}\x1b[0m` + ); + delete tribesdns[tribe]; + } + }); + // A partial/restored adminapi.json must not crash the whole server: fill + // the runtime configuration from the install template and warn loudly. + const initconf = fs.readJsonSync( + "../adminapi/apxtri/setup/initadminapi.json" ); - const tribesdns = fs.readJsonSync( - `../adminapi/objects/tribes/idx/tribes_dns.json` - ); - //check if new tribe was add - glob + if (!conf.api) { + conf.api = JSON.parse(JSON.stringify(initconf.api)); + console.log( + "\x1b[31m adminapi.json is missing 'api', using defaults from apxtri/setup/initadminapi.json. Restore this file from a backup.\x1b[0m" + ); + } + if (!conf.socket) { + conf.socket = JSON.parse(JSON.stringify(initconf.socket)); + console.log( + "\x1b[31m adminapi.json is missing 'socket', using default port from apxtri/setup/initadminapi.json.\x1b[0m" + ); + } + //check if new tribe was add (only real tribe folders, never tmp/.git/...) + const tribedirs = glob .sync("../*") .filter((f) => fs.lstatSync(f).isDirectory()) - .forEach(async (t) => { - const tribe = path.basename(t); - //console.log(tribe); - if (![".", ".."].includes(tribe) && !tribesdns[tribe]) { - await apxtri.setuptribe(tribe, conf); + .filter((f) => fs.existsSync(`${f}/apxtri/routes`)); + for (const t of tribedirs) { + const tribe = path.basename(t); + if ([".", ".."].includes(tribe) || tribesdns[tribe]) continue; + // Never re-initialize an already configured tribe: if its itm file + // exists, just restore its dns entry instead of overwriting the config. + const itmpath = `../adminapi/objects/tribes/itm/${tribe}.json`; + if (fs.existsSync(itmpath)) { + try { + const existing = fs.readJSONSync(itmpath); + if (Array.isArray(existing.dns) && existing.dns.length > 0) { + tribesdns[tribe] = existing.dns; + console.log( + `tribe ${tribe} already configured, restored in tribes_dns.json` + ); + continue; + } + } catch (err) { + console.log( + `unreadable itm/${tribe}.json, skipping:`, + err.message + ); + continue; } - }); + } + await apxtri.setuptribe(tribe, conf); + } + // merge with the on-disk index so setuptribe writes are preserved + const ondiskdns = fs.existsSync(tribednspath) + ? fs.readJsonSync(tribednspath) + : {}; + Object.keys(ondiskdns).forEach((tribe) => { + if (!apxtri.istribe(tribe)) delete ondiskdns[tribe]; + }); + fs.outputJSONSync( + tribednspath, + Object.assign({}, ondiskdns, tribesdns), + { space: 2 } + ); + // run api with the (possibly repaired) in-memory conf + await apxtri.runexpress(fs.readJsonSync(tribednspath), conf); } else { const initconf = fs.readJsonSync( "../adminapi/apxtri/setup/initadminapi.json" ); await apxtri.setuptribe("adminapi", initconf); + // run api with update conf + await apxtri.runexpress( + fs.readJsonSync(`../adminapi/objects/tribes/idx/tribes_dns.json`), + fs.readJSONSync("../adminapi/objects/tribes/itm/adminapi.json") + ); } - // run api with update conf - apxtri.runexpress( - fs.readJsonSync(`../adminapi/objects/tribes/idx/tribes_dns.json`), - fs.readJSONSync("../adminapi/objects/tribes/itm/adminapi.json") - ); }; apxtri.getip = async () => { /** @@ -112,6 +178,21 @@ apxtri.getip = async () => { return network; }; apxtri.setuptribe = async (tribe, conf) => { + if (!apxtri.istribe(tribe)) { + console.log( + `\x1b[31m skip setuptribe: ${tribe} is not an existing tribe folder\x1b[0m` + ); + return; + } + if (tribe != "adminapi") { + const existingitm = `../adminapi/objects/tribes/itm/${tribe}.json`; + if (fs.existsSync(existingitm)) { + console.log( + `\x1b[31m skip setuptribe: ${tribe} is already configured\x1b[0m` + ); + return; + } + } let inittribe; if (tribe == "adminapi") { console.log( @@ -131,9 +212,13 @@ apxtri.setuptribe = async (tribe, conf) => { } } else { console.log(`a new tribe called ${tribe} was detected`); - const adminapiconf = fs.readJSONSync( - "../adminapi/objects/tribes/itm/adminapi.json" + const initconf = fs.readJsonSync( + "../adminapi/apxtri/setup/initadminapi.json" ); + const adminapiconf = + conf && conf.api && conf.socket + ? conf + : fs.readJSONSync("../adminapi/objects/tribes/itm/adminapi.json"); inittribe = { tribeId: tribe, townpath: __dirname.replace("/adminapi/apxtri", ""), @@ -142,8 +227,8 @@ apxtri.setuptribe = async (tribe, conf) => { nationId: conf.nationId, townId: conf.townId, activelog: [], - api: { port: adminapiconf.api.port }, - socket: { port: adminapiconf.socket.port }, + api: { port: (adminapiconf.api || initconf.api).port }, + socket: { port: (adminapiconf.socket || initconf.socket).port }, }; } inittribe.sudoUser = process.env.USER; @@ -171,7 +256,7 @@ apxtri.setuptribe = async (tribe, conf) => { let lsttribe = fs.existsSync(lst_tribeIdpath) ? fs.readJsonSync(lst_tribeIdpath) : []; - lsttribe.push(tribe); + if (!lsttribe.includes(tribe)) lsttribe.push(tribe); fs.outputJSONSync(lst_tribeIdpath, lsttribe); const tribes_dnspath = "../adminapi/objects/tribes/idx/tribes_dns.json"; const tribedns = fs.existsSync(tribes_dnspath) @@ -248,7 +333,12 @@ apxtri.setuptribe = async (tribe, conf) => { apxtri.runexpress = async (tribesdns, conf) => { const Odmdb = require(path.resolve("./apxtri/models/Odmdb.js")); - let tribeIds = Object.keys(tribesdns); + const checkRequestSafety = require( + path.resolve("./apxtri/middlewares/checkRequestSafety.js") + ); + let tribeIds = Object.keys(tribesdns).filter((tribe) => + apxtri.istribe(tribe) + ); // context is store in /itm/tribename.json ={contexte:{routes:[],models:[{model:,tplstringslg:[]}]} // routes={url,route} check how to add plugin tribe route later // keep only the 2 last part (.) of domain name to validate requests with it (generic domain) @@ -260,6 +350,13 @@ apxtri.runexpress = async (tribesdns, conf) => { if (!doms.includes(dm)) doms.push(dm); //reindex database attention check dev-ants/.. a bug was fixed glob.sync(`../${t}/objects/*`).forEach((o) => { + if (!fs.existsSync(o) || !fs.lstatSync(o).isDirectory()) return; + if ( + !fs.existsSync(path.join(o, "itm")) && + !fs.existsSync(path.join(o, "conf.json")) + ) { + return; + } console.log("reindex: ", o); Odmdb.runidx(o); }); @@ -292,18 +389,34 @@ apxtri.runexpress = async (tribesdns, conf) => { }); const app = express(); // load express parameter from conf - Object.keys(conf.api.appset).forEach((p) => { - app.set(p, conf.api.appset[p]); + const apiconf = conf.api || {}; + if (apiconf.appset && apiconf.appset["trust proxy"] === true) { + apiconf.appset["trust proxy"] = 1; + console.log( + "\x1b[31m api.appset['trust proxy']=true is unsafe, using 1 (nginx only)\x1b[0m" + ); + } + Object.keys(apiconf.appset || {}).forEach((p) => { + app.set(p, apiconf.appset[p]); }); // To set depending of data form or get size to send tribeIds.forEach((t) => { - app.use(`/${t}`, bodyParser.urlencoded(conf.api.bodyparse.urlencoded)); - app.use(`/${t}`, bodyParser.json(conf.api.bodyparse.json)); + app.use( + `/${t}`, + bodyParser.urlencoded( + (apiconf.bodyparse && apiconf.bodyparse.urlencoded) || {}, + ), + ); + app.use( + `/${t}`, + bodyParser.json((apiconf.bodyparse && apiconf.bodyparse.json) || {}), + ); // To set depending of post put json data size to send - app.use(`/${t}`, express.json(conf.api.json)); + app.use(`/${t}`, express.json(apiconf.json || {})); }); app.disable("x-powered-by"); // for security app.locals.tribeids = tribeIds; + app.use(checkRequestSafety); const currentmod = "apxtri"; const log = process.env.ACTIVELOG ? process.env.ACTIVELOG.split(',').includes(currentmod) : false; console.log( diff --git a/apxtri/middlewares/checkHeaders.js b/apxtri/middlewares/checkHeaders.js index 5517dd4..f83b140 100755 --- a/apxtri/middlewares/checkHeaders.js +++ b/apxtri/middlewares/checkHeaders.js @@ -60,6 +60,14 @@ const log = process.env.ACTIVELOG ? process.env.ACTIVELOG.split(',').includes(cu * data: {xalias} * } */ +const aliaspattern = /^[a-z0-9]{3,64}$/; +const namepattern = /^[A-Za-z0-9_.:-]{1,64}$/; +const indexpattern = /^[A-Za-z0-9_.-]{1,128}$/; +const uuidpattern = + /^(0|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/; +const emailpattern = + /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + const checkHeaders = (req, res, next) => { req.session = {}; const header = {}; @@ -90,14 +98,118 @@ const checkHeaders = (req, res, next) => { data: {missingheader}, }); } + const allowedtribes = [ + "town", + "adminapi", + ...(Array.isArray(req.app.locals.tribeids) ? req.app.locals.tribeids : []), + ]; + const invalidheader = (field, value) => + res.status(400).json({ + status: 400, + ref: "middlewares", + msg: "headervalueinvalid", + data: { field, value }, + }); + if (header.xalias !== "anonymous" && !aliaspattern.test(header.xalias)) { + return invalidheader("xalias", header.xalias); + } + if (!uuidpattern.test(header.xuuid)) { + return invalidheader("xuuid", header.xuuid); + } + if ( + header.xapp === "." || + header.xapp === ".." || + !namepattern.test(header.xapp) + ) { + return invalidheader("xapp", header.xapp); + } + if (header.xdays !== undefined && !/^\d{0,16}$/.test(header.xdays)) { + return invalidheader("xdays", header.xdays); + } + if ( + header.xtrkversion !== undefined && + !/^\d{0,10}$/.test(header.xtrkversion) + ) { + return invalidheader("xtrkversion", header.xtrkversion); + } + if ( + req.headers.xworkon !== undefined && + !allowedtribes.includes(req.headers.xworkon) + ) { + return res.status(404).json({ + status: 404, + ref: "middlewares", + msg: "tribeiddoesnotexist", + data: { xworkon: req.headers.xworkon }, + }); + } + const invalidparam = (param, value) => + res.status(400).json({ + status: 400, + ref: "middlewares", + msg: "paramvalueinvalid", + data: { param, value }, + }); + for (const param of ["tribe", "tribeId", "tribeid"]) { + if ( + req.params[param] !== undefined && + !allowedtribes.includes(req.params[param]) + ) { + return res.status(404).json({ + status: 404, + ref: "middlewares", + msg: "tribeiddoesnotexist", + data: { tribe: req.params[param] }, + }); + } + } + const paramrules = { + alias: aliaspattern, + objectname: indexpattern, + optionname: indexpattern, + indexname: indexpattern, + primaryindex: /^[A-Za-z0-9_.@+-]{1,128}$/, + primaryid: /^[A-Za-z0-9_.@+-]{1,128}$/, + apxid: /^[A-Za-z0-9_]{1,64}$/, + articleid: indexpattern, + evaluation: /^(follow|[0-4])$/, + app: namepattern, + key: namepattern, + jobadid: indexpattern, + jobstepid: indexpattern, + personalias: aliaspattern, + recruiter: aliaspattern, + seeker: aliaspattern, + profil: namepattern, + wconame: namepattern, + appname: namepattern, + pagename: namepattern, + website: namepattern, + template: namepattern, + mlist: namepattern, + srckey: namepattern, + action: namepattern, + pluginname: namepattern, + filename: namepattern, + email: emailpattern, + siret: /^\d{14}$/, + typekey: /^[A-Za-z0-9_.-]{1,32}$/, + data: /^[A-Za-z0-9_.@+-]{1,128}$/, + validation: /^[A-Za-z0-9_-]{1,128}$/, + digit6: /^\d{1,6}$/, + version: /^\d{1,16}$/, + }; + for (const param of Object.keys(paramrules)) { + const value = req.params[param]; + if (value === undefined || value === "") continue; + if (value === "." || value === ".." || !paramrules[param].test(value)) { + return invalidparam(param, value); + } + } //console.log( req.app.locals.tribeids ) // xtribe == "town" is used during the setup process // xtribe == "adminapi" is used to access /adminapi - if ( - !( - ["town","adminapi"].includes(header.xtribe) || req.app.locals.tribeids.includes(header.xtribe) - ) - ) { + if (!allowedtribes.includes(header.xtribe)) { return res.status(404).json({ status:404, ref: "middlewares", diff --git a/apxtri/middlewares/checkRequestSafety.js b/apxtri/middlewares/checkRequestSafety.js new file mode 100644 index 0000000..656e37b --- /dev/null +++ b/apxtri/middlewares/checkRequestSafety.js @@ -0,0 +1,67 @@ +const Security = require("../models/Security.js"); + +const currentmod = "checkRequestSafety"; +const log = process.env.ACTIVELOG + ? process.env.ACTIVELOG.split(",").includes(currentmod) + : false; + +const pathHeaders = ["xtribe", "xapp", "xuuid", "xalias", "xworkon"]; +const enforce = process.env.SECURITY_TRAVERSAL_ENFORCE === "true"; + +const collect = (value, field, findings, depth = 0) => { + if (depth > 8 || findings.length >= 5) return; + if (typeof value === "string") { + if (Security.freeTextFields.includes(field)) return; + if (/^imgbase64_/i.test(field)) return; + if (Security.isTraversal(value)) findings.push({ field, value }); + return; + } + if (Array.isArray(value)) { + value.slice(0, 100).forEach((item) => collect(item, field, findings, depth + 1)); + return; + } + if (value && typeof value === "object") { + Object.keys(value) + .slice(0, 100) + .forEach((key) => collect(value[key], key, findings, depth + 1)); + } +}; + +const checkRequestSafety = (req, res, next) => { + const findings = []; + collect(req.originalUrl, "url", findings); + collect(req.path, "path", findings); + collect(req.params, "params", findings); + collect(req.query, "query", findings); + collect(req.body, "body", findings); + pathHeaders.forEach((header) => { + const value = req.headers[header]; + if (value !== undefined && Security.isTraversal(value)) { + findings.push({ field: header, value }); + } + }); + + const unique = []; + findings.forEach((finding) => { + const key = `${finding.field}|${finding.value}`; + if (!unique.some((item) => `${item.field}|${item.value}` === key)) { + unique.push(finding); + } + }); + + if (unique.length > 0) { + if (log) console.log(currentmod, "suspicious request", unique); + unique.forEach((finding) => Security.alert(req, finding)); + if (enforce) { + return res.status(400).json({ + status: 400, + ref: "middlewares", + msg: "invalidrequest", + data: { fields: unique.map((finding) => finding.field) }, + }); + } + } + next(); +}; + +module.exports = checkRequestSafety; diff --git a/apxtri/middlewares/isAuthenticated.js b/apxtri/middlewares/isAuthenticated.js index 1192261..2477013 100755 --- a/apxtri/middlewares/isAuthenticated.js +++ b/apxtri/middlewares/isAuthenticated.js @@ -117,13 +117,33 @@ const isAuthenticated = async (req, res, next) => { if (log) console.log(currentmod,"time out penalty"); } }; + const paganpath = `../adminapi/objects/pagans/itm/${req.session.header.xalias}.json`; + const paganexists = fs.existsSync(paganpath); + const base = req.baseUrl || ""; + const ispaganroute = /\/pagans\/?$/.test(base); + const iscreatepagan = + req.method === "POST" && + ispaganroute && + (req.path === "/" || req.path === ""); + if (!paganexists && !iscreatepagan) { + resnotauth.status = 401; + resnotauth.data.xaliasexists = false; + return res.status(resnotauth.status).json(resnotauth); + } + if (iscreatepagan && req.body.alias !== req.session.header.xalias) { + return res.status(400).json({ + status: 400, + ref: "Middlewares", + msg: "aliasmismatch", + data: { xalias: req.session.header.xalias, alias: req.body.alias }, + }); + } if (!fs.existsSync(tmpfs)) { // need to check detached sign let publickey = ""; - const aliasinfo = `../adminapi/objects/pagans/itm/${req.session.header.xalias}.json`; - if (fs.existsSync(aliasinfo)) { - publickey = fs.readJsonSync(aliasinfo).publickey; - } else if (req.body.publickey) { + if (paganexists) { + publickey = fs.readJsonSync(paganpath).publickey; + } else if (iscreatepagan && req.body.publickey) { resnotauth.data.xaliasexists = false; publickey = req.body.publickey; } diff --git a/apxtri/models/Checkjson.js b/apxtri/models/Checkjson.js index 99b700c..6be2a78 100755 --- a/apxtri/models/Checkjson.js +++ b/apxtri/models/Checkjson.js @@ -106,7 +106,8 @@ Checkjson.schema.properties.format = { ipv4: /^([0–9]{1,3}.){3}.([0–9]{1,3})$/, ipv6: /^((([0–9A-Fa-f]{1,4}:){7}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){6}:[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){5}:([0–9A-Fa-f]{1,4}:)?[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){4}:([0–9A-Fa-f]{1,4}:){0,2}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){3}:([0–9A-Fa-f]{1,4}:){0,3}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){2}:([0–9A-Fa-f]{1,4}:){0,4}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){6}((b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b).){3}(b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b))|(([0–9A-Fa-f]{1,4}:){0,5}:((b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b).){3}(b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b))|(::([0–9A-Fa-f]{1,4}:){0,5}((b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b).){3}(b((25[0–5])|(1d{2})|(2[0–4]d)|(d{1,2}))b))|([0–9A-Fa-f]{1,4}::([0–9A-Fa-f]{1,4}:){0,5}[0–9A-Fa-f]{1,4})|(::([0–9A-Fa-f]{1,4}:){0,6}[0–9A-Fa-f]{1,4})|(([0–9A-Fa-f]{1,4}:){1,7}:))$/, telephonefr: /^0[1-9][0-9]{8}$/, - imgbase64: /data:image\/(png|jpg|jpeg|svg);base64,(?:[A-Za-z0-9+\/]+)/, + imgbase64: + /^data:image\/(png|jpeg|jpg|webp|gif);base64,[A-Za-z0-9+/]+={0,2}$/i, telephoneinter: /^\+*(\d{3})*[0-9,\-]{8,}/, password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&.])[A-Za-z\d$@$!%*?&.{}:|\s]{8,}/, diff --git a/apxtri/models/Notifications.js b/apxtri/models/Notifications.js index ab18aeb..02230e8 100644 --- a/apxtri/models/Notifications.js +++ b/apxtri/models/Notifications.js @@ -21,7 +21,18 @@ const log = process.env.ACTIVELOG const Notifications = {}; Notifications.get = (alias, tribeId) => { - const notiffile = `../${req.params.tribe}/notifications/${req.params.alias}.json`; + if ( + !/^[a-z0-9]{3,64}$/.test(String(alias)) || + !/^[a-z0-9_-]{1,64}$/.test(String(tribeId)) + ) { + return { + status: 400, + ref: "Notification", + msg: "paramsinvalid", + data: { alias, tribeId }, + }; + } + const notiffile = `../${tribeId}/notifications/${alias}.json`; const msg = fs.existsSync(notiffile) ? fs.readJSONSync(notiffile) : {}; return { status: 200, @@ -50,8 +61,6 @@ Notifications.statmaillist = (tribe) => { }); }); }); - // fichier csv stocker en local en attendant d'avoir un back pour stocker la reponse dans data.csv - fs.outputFileSync(`../${tribe}/mailinglst.csv`, csv, "utf-8"); return { status: 200, ref: "Notifications", diff --git a/apxtri/models/Odmdb.js b/apxtri/models/Odmdb.js index ee373c9..e653ece 100644 --- a/apxtri/models/Odmdb.js +++ b/apxtri/models/Odmdb.js @@ -625,6 +625,145 @@ Odmdb.indexAllowed = (indexaccess, role) => { * */ +Odmdb.prepareUploadedImages = async (objectPathname, itm) => { + const getschema = Odmdb.Schema(objectPathname, true); + if (getschema.status != 200) return getschema; + const schema = getschema.data.schema; + const imagekeys = Object.keys(itm).filter( + (key) => key.startsWith("imgbase64_") && itm[key] !== "" + ); + if (imagekeys.length == 0) { + return { + status: 200, + ref: "Odmdb", + msg: "imagesprepared", + data: { written: [] }, + }; + } + const apxidvalue = itm[schema.apxid]; + if (!apxidvalue) { + return { + status: 406, + ref: "Odmdb", + msg: "apxidmissing", + data: { missingkey: schema.apxid }, + }; + } + const allowed = /^data:image\/(png|jpeg|jpg|webp|gif);base64,([A-Za-z0-9+/=\s]+)$/i; + const validmagic = (buffer) => { + if (buffer.length < 12) return false; + const ispng = + buffer[0] === 0x89 && + buffer[1] === 0x50 && + buffer[2] === 0x4e && + buffer[3] === 0x47 && + buffer[4] === 0x0d && + buffer[5] === 0x0a && + buffer[6] === 0x1a && + buffer[7] === 0x0a; + const isjpeg = + buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff; + const gif = buffer.slice(0, 6).toString(); + const isgif = gif === "GIF87a" || gif === "GIF89a"; + const iswebp = + buffer.slice(0, 4).toString() === "RIFF" && + buffer.slice(8, 12).toString() === "WEBP"; + return ispng || isjpeg || isgif || iswebp; + }; + const sharp = require("sharp"); + const objectname = path.basename(objectPathname); + const imgdir = path.resolve(objectPathname, "img"); + const safeid = String(apxidvalue) + .replace(/[^A-Za-z0-9_-]/g, "_") + .slice(0, 64); + const writes = []; + for (const key of imagekeys) { + if (typeof itm[key] !== "string") { + return { + status: 422, + ref: "Odmdb", + msg: "imageinvalid", + data: { field: key }, + }; + } + const match = itm[key].match(allowed); + if (!match) { + return { + status: 422, + ref: "Odmdb", + msg: "imageinvalid", + data: { field: key }, + }; + } + const newk = key.slice("imgbase64_".length); + if (!schema.properties[newk]) { + return { + status: 422, + ref: "Odmdb", + msg: "imgpropertiesmissing", + data: { field: key, propertie: newk }, + }; + } + const buffer = Buffer.from(match[2].replace(/\s+/g, ""), "base64"); + if (buffer.length == 0 || buffer.length > 5 * 1024 * 1024) { + return { + status: 422, + ref: "Odmdb", + msg: "imageinvalid", + data: { field: key, size: buffer.length }, + }; + } + if (!validmagic(buffer)) { + return { + status: 422, + ref: "Odmdb", + msg: "imageinvalid", + data: { field: key }, + }; + } + let webp; + try { + webp = await sharp(buffer, { limitInputPixels: 25000000 }) + .rotate() + .webp({ quality: 80 }) + .toBuffer(); + } catch (err) { + return { + status: 422, + ref: "Odmdb", + msg: "imageinvalid", + data: { field: key, error: err.message }, + }; + } + const safek = String(newk) + .replace(/[^A-Za-z0-9_-]/g, "_") + .slice(0, 64); + const filename = `apximg_${safeid}_${safek}.webp`; + const finalpath = path.resolve(imgdir, filename); + if (!finalpath.startsWith(`${imgdir}${path.sep}`)) { + return { + status: 422, + ref: "Odmdb", + msg: "imagepathinvalid", + data: { field: key }, + }; + } + writes.push({ key, newk, filename, webp, finalpath }); + } + fs.ensureDirSync(imgdir); + writes.forEach((write) => { + fs.outputFileSync(write.finalpath, write.webp); + itm[write.newk] = `objectimg/${objectname}/img/${write.filename}`; + delete itm[write.key]; + }); + return { + status: 200, + ref: "Odmdb", + msg: "imagesprepared", + data: { written: writes.map((write) => write.finalpath) }, + }; +}; + Odmdb.cud = (objectPathname, crud, itm, role, runindex = true) => { const getschema = Odmdb.Schema(objectPathname, true); if (getschema.status != 200) return getschema; @@ -637,6 +776,17 @@ Odmdb.cud = (objectPathname, crud, itm, role, runindex = true) => { data: { missingkey: getschema.data.schema.apxid }, }; } + const pendingimages = Object.keys(itm).filter( + (key) => key.startsWith("imgbase64_") && itm[key] !== "" + ); + if (pendingimages.length > 0) { + return { + status: 422, + ref: "Odmdb", + msg: "imgbase64requirespreprocessing", + data: { fields: pendingimages }, + }; + } fs.ensureDirSync(`${objectPathname}/itm/`); fs.ensureDirSync(`${objectPathname}/idx/`); const existid = fs.existsSync( @@ -774,40 +924,6 @@ Odmdb.cud = (objectPathname, crud, itm, role, runindex = true) => { //console.log(k, itmtostore[k], itmtostore[k] === ""); if (itmtostore[k] === "") delete itmtostore[k]; }); - Object.keys(itmtostore).forEach((k) => { - //Manage base64 case image to convert ans store in webp - if (k.includes("imgbase64_") && itmtostore[k] != "") { - fs.ensureDirSync(`${objectPathname}/img/`); - //console.log("check this for k:", k, " itmtostore[k]:", itmtostore[k]); - const imgb64 = itmtostore[k].replace( - /^data:image\/(png|png|gif|bmp|jpg|jpeg);base64,/, - "" - ); //.replace(/\+/g," "); - const extension = itmtostore[k].split(";base64,")[0].split("/")[1]; - const newk = k.split("_").slice(1).join("_"); - const filenameimg = `/img/${ - itmtostore[getschema.data.schema.apxid] - }_${newk}.${extension}`; - if (!getschema.data.schema.properties[newk]) { - if (log) - console.log( - currentmod, - " Check your schema it miss a propertie because imgbase64_blabla must have a blabla propertie to host the pathfile image" - ); - } else { - const objectname = objectPathname.split("/").slice(-1)[0]; - itmtostore[newk] = `objectimg/${objectname}${filenameimg}`; - //const webp = require("webp-converter"); - //const buf = Buffer.from(itmtostore[k], 'base64'); - //const imgwebp = await webp.bufferToWebp(buf); - //@TODO use sharp to resize img with attribut sizeHW in propertie - fs.writeFileSync(`${objectPathname}/${filenameimg}`, imgb64, { - encoding: "base64", - }); - delete itmtostore[k]; - } - } - }); // check consistency of datatostore //console.log("itmstostore::::::", itmtostore); const chkdata = Checkjson.schema.data( @@ -858,7 +974,18 @@ Odmdb.cud = (objectPathname, crud, itm, role, runindex = true) => { * */ Odmdb.runidx = (objectPathname, schema) => { - const starttime=process.hrtime.bigint(); + const starttime=process.hrtime.bigint(); + if ( + !fs.existsSync(objectPathname) || + !fs.lstatSync(objectPathname).isDirectory() + ) { + return { + status: 404, + ref: "Odmdb", + msg: "objectdoesnotexist", + data: { objectPathname }, + }; + } if (!schema || !schema.apxid) { const getschema = Odmdb.Schema(objectPathname, true); if (getschema.status != 200) { @@ -867,6 +994,14 @@ Odmdb.runidx = (objectPathname, schema) => { } schema = getschema.data.schema; } + if (!Array.isArray(schema.apxidx)) { + return { + status: 406, + ref: "Odmdb", + msg: "apxidxmissing", + data: { objectPathname }, + }; + } const ventil = {}; schema.apxidx.forEach((id) => { ventil[id.name] = id; @@ -881,7 +1016,7 @@ Odmdb.runidx = (objectPathname, schema) => { } else { if (!schema.properties[id.keyval]){ console.log(`Check ${objectPathname} index ${id.name} property ${id.keyval} does not exist`) - process.exit(0) + return } ventil[id.name].isunique = schema.apxuniquekey.includes(id.keyval); ventil[id.name].nbobjkey = id.objkey ? id.objkey.length : 0; diff --git a/apxtri/models/Pagans.js b/apxtri/models/Pagans.js index bee72ca..d2540e3 100644 --- a/apxtri/models/Pagans.js +++ b/apxtri/models/Pagans.js @@ -84,6 +84,20 @@ Pagans.keyrecovery = async (emailalias, tribe, search, lg) => { data: { emailalias, tribe, search, lg }, }; } + const searchvalid = + emailalias === "alias" + ? /^[a-z0-9]{3,64}$/.test(String(search)) + : /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test( + String(search), + ); + if (!/^[a-z0-9_-]{1,64}$/.test(String(tribe)) || !searchvalid) { + return { + status: 400, + ref: "Pagans", + msg: "searchinvalid", + data: { emailalias, tribe, search }, + }; + } let emaillist = []; if (emailalias == "email") { search = search.toLowerCase(); @@ -148,6 +162,14 @@ Pagans.keyrecovery = async (emailalias, tribe, search, lg) => { * **/ Pagans.getalias = (alias) => { + if (!/^[a-z0-9]{3,64}$/.test(String(alias))) { + return { + status: 400, + ref: "Pagans", + msg: "aliasinvalid", + data: { alias }, + }; + } //bypass Odmdb cause all is public save ressources console.log(path.resolve(`../adminapi/objects/pagans/itm/${alias}.json`)); if (fs.existsSync(`../adminapi/objects/pagans/itm/${alias}.json`)) { diff --git a/apxtri/models/Security.js b/apxtri/models/Security.js new file mode 100644 index 0000000..b0ebe5f --- /dev/null +++ b/apxtri/models/Security.js @@ -0,0 +1,139 @@ +const crypto = require("crypto"); +const conf = require(`../../../adminapi/objects/tribes/itm/adminapi.json`); + +const Security = {}; + +Security.freeTextFields = [ + "biography", + "title", + "description", + "message", + "html", + "text", + "content", + "comment", + "subject", +]; + +Security.hash = (value) => + crypto + .createHash("sha256") + .update(String(value === undefined ? "" : value)) + .digest("hex") + .slice(0, 16); + +Security.normalize = (value) => { + let normalized = String(value); + for (let i = 0; i < 3; i++) { + try { + const decoded = decodeURIComponent(normalized); + if (decoded === normalized) break; + normalized = decoded; + } catch (err) { + break; + } + } + return normalized.replace(/\\/g, "/"); +}; + +Security.isTraversal = (value) => { + if (typeof value !== "string" || value === "") return false; + const normalized = Security.normalize(value); + if (normalized.includes("\0")) return true; + return normalized + .split("/") + .some((segment) => segment === ".." || segment === "..;"); +}; + +Security.redact = (value, depth = 0) => { + if (depth > 6) return "[depthlimit]"; + if (value === null || value === undefined) return value; + if (typeof value === "string") { + return value.length > 2000 ? `${value.slice(0, 2000)}...[truncated]` : value; + } + if (Array.isArray(value)) { + return value.slice(0, 50).map((item) => Security.redact(item, depth + 1)); + } + if (typeof value === "object") { + const redacted = {}; + Object.keys(value) + .slice(0, 100) + .forEach((key) => { + if (/privatekey|passphrase|xhash|password|secret|token|apikey|smtp|brevo/i.test(key)) { + redacted[key] = "[redacted]"; + } else if (/^imgbase64_/i.test(key)) { + redacted[key] = "[image]"; + } else { + redacted[key] = Security.redact(value[key], depth + 1); + } + }); + return redacted; + } + return value; +}; + +Security.throttlekey = (ip, field, value) => + `${ip}|${field}|${Security.hash(value)}`; + +Security.throttled = new Map(); +Security.emailtimes = []; + +Security.alert = (req, details = {}) => { + const header = (req.session && req.session.header) || {}; + const ip = + req.ip || (req.socket && req.socket.remoteAddress) || "unknown"; + const field = details.field || "unknown"; + const value = + typeof details.value === "string" + ? details.value.slice(0, 500) + : details.value; + const now = Date.now(); + const throttlekey = Security.throttlekey(ip, field, value); + const last = Security.throttled.get(throttlekey) || 0; + if (now - last < 10 * 60 * 1000) return; + Security.throttled.set(throttlekey, now); + if (Security.throttled.size > 5000) { + Security.throttled.forEach((ts, key) => { + if (now - ts > 10 * 60 * 1000) Security.throttled.delete(key); + }); + } + + const entry = { + at: new Date(now).toISOString(), + method: req.method, + url: req.originalUrl, + ip, + xtribe: header.xtribe, + xalias: header.xalias, + field, + value, + body: Security.redact(req.body), + }; + console.log("ALERT security", JSON.stringify(entry)); + + const emailalertsecu = + conf && typeof conf.emailalertsecu === "string" + ? conf.emailalertsecu.trim() + : ""; + if (!emailalertsecu) return; + Security.emailtimes = Security.emailtimes.filter( + (ts) => now - ts < 60 * 60 * 1000 + ); + if (Security.emailtimes.length >= 30) return; + Security.emailtimes.push(now); + + const Notifications = require("./Notifications.js"); + Notifications.sendmail( + { + to: emailalertsecu, + subject: `[apXtri security] ${field} - ${req.method} ${req.originalUrl}`, + text: JSON.stringify(entry, null, 2), + html: `
${JSON.stringify(entry, null, 2)}`,
+ },
+ "adminapi"
+ ).catch((err) => {
+ console.log("ALERT security email failed", err.message);
+ });
+};
+
+module.exports = Security;
diff --git a/apxtri/models/Wwws.js b/apxtri/models/Wwws.js
index 2fe00fc..c7ccf94 100644
--- a/apxtri/models/Wwws.js
+++ b/apxtri/models/Wwws.js
@@ -16,6 +16,24 @@ const log = process.env.ACTIVELOG ? process.env.ACTIVELOG.split(',').includes(cu
const Wwws = {};
Wwws.getwco = (wconame, ctx) => {
+ const safename = (value) =>
+ typeof value === "string" && /^[A-Za-z0-9_.:-]{1,64}$/.test(value);
+ const tagids = ctx.tagid ? String(ctx.tagid).split(",") : [];
+ if (
+ !safename(wconame) ||
+ !safename(ctx.tribe) ||
+ !safename(ctx.wcotribe) ||
+ !safename(ctx.xapp) ||
+ !safename(ctx.pagename) ||
+ tagids.some((tagid) => !safename(tagid))
+ ) {
+ return {
+ status: 400,
+ ref: "Wwws",
+ msg: "queryinvalid",
+ data: { wconame, query: ctx },
+ };
+ }
/**
* In a webpage to add a wco
*
@@ -486,6 +504,22 @@ Wwws.initlocalwco = (tribwco, profils, lg) => {
};
Wwws.initlocaldata = (tribe, appname, pagename, version, profils, lg) => {
+ const safename = (value) =>
+ typeof value === "string" && /^[A-Za-z0-9_.:-]{1,64}$/.test(value);
+ if (
+ !safename(tribe) ||
+ !safename(appname) ||
+ !safename(pagename) ||
+ !/^[A-Za-z]{2}$/.test(String(lg)) ||
+ !/^\d{1,16}$/.test(String(version))
+ ) {
+ return {
+ status: 400,
+ ref: "Wwws",
+ msg: "paraminvalid",
+ data: { tribe, appname, pagename, version, lg },
+ };
+ }
const fileparam = `../${tribe}/objects/wwws/itm/${appname}.json`;
//console.log(path.resolve(fileparam));
if (!fs.existsSync(fileparam)) {
diff --git a/apxtri/routes/notifications.js b/apxtri/routes/notifications.js
index f279c16..b35b172 100644
--- a/apxtri/routes/notifications.js
+++ b/apxtri/routes/notifications.js
@@ -1,9 +1,13 @@
const express = require("express");
+const crypto = require("crypto");
const fs = require("fs-extra");
const path = require("path");
// Classes
const Notifications = require("../models/Notifications.js");
+const emailpattern =
+ /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+
// Middlewares
const checkHeaders = require("../middlewares/checkHeaders");
const isAuthenticated = require("../middlewares/isAuthenticated");
@@ -26,7 +30,7 @@ const router = express.Router();
**/
router.get("/messages/:alias/:tribeId", (req, res) => {
const getnot = Notifications.get(req.params.alias, req.params.tribeId);
- res.status(getalias.status).send(getalias);
+ res.status(getnot.status).json(getnot);
});
/**
@@ -119,6 +123,34 @@ router.post("/registeranonymous", checkHeaders, (req, res) => {
data: { typekey: req.body.typekey },
});
}
+ const allowedtribes = [
+ "adminapi",
+ ...(Array.isArray(req.app.locals.tribeids) ? req.app.locals.tribeids : []),
+ ];
+ if (!allowedtribes.includes(req.body.tribe)) {
+ return res.status(404).json({
+ status: 404,
+ ref: "Notifications",
+ msg: "tribeiddoesnotexist",
+ data: { tribe: req.body.tribe },
+ });
+ }
+ if (!/^[A-Za-z0-9_.-]{1,64}$/.test(req.body.mlist || "")) {
+ return res.status(400).json({
+ status: 400,
+ ref: "Notifications",
+ msg: "mlistinvalid",
+ data: { mlist: req.body.mlist },
+ });
+ }
+ if (!/^[A-Za-z0-9_.-]{1,64}$/.test(req.body.srckey || "")) {
+ return res.status(400).json({
+ status: 400,
+ ref: "Notifications",
+ msg: "srckeyinvalid",
+ data: { srckey: req.body.srckey },
+ });
+ }
const key = req.body.contactpoint
? req.body.contactpoint
: req.body[req.body.typekey];
@@ -201,7 +233,18 @@ router.get(
*
*/
router.get("/stat/maillinglst/:tribe", checkHeaders, async (req, res) => {
- console.log("passe là");
+ const allowedtribes = [
+ "adminapi",
+ ...(Array.isArray(req.app.locals.tribeids) ? req.app.locals.tribeids : []),
+ ];
+ if (!allowedtribes.includes(req.params.tribe)) {
+ return res.status(404).json({
+ status: 404,
+ ref: "Notifications",
+ msg: "tribeiddoesnotexist",
+ data: { tribe: req.params.tribe },
+ });
+ }
const resstat = Notifications.statmaillist(req.params.tribe);
res.status(resstat.status).json(resstat);
});
@@ -270,8 +313,19 @@ router.get("/contact", checkHeaders, isAuthenticated, (req, res) => {
*/
router.get("/checkemail/:email/:digit6", checkHeaders, async (req, res) => {
const { email, digit6 } = req.params;
+ if (!emailpattern.test(email)) {
+ return res.status(400).json({
+ status: 400,
+ ref: "Notifications",
+ msg: "emailinvalid",
+ data: { email },
+ });
+ }
const tmpDir = path.join(__dirname, "../../tmp");
- const tmpFile = path.join(tmpDir, email);
+ const tmpFile = path.join(
+ tmpDir,
+ `${crypto.createHash("sha256").update(email).digest("hex")}.txt`
+ );
if (digit6 === "0") {
const code = email.includes("smatchit.io") ? "789000" : Math.floor(100000 + Math.random() * 900000).toString();
diff --git a/apxtri/routes/odmdb.js b/apxtri/routes/odmdb.js
index acd2787..80dfb56 100644
--- a/apxtri/routes/odmdb.js
+++ b/apxtri/routes/odmdb.js
@@ -82,7 +82,7 @@ router.get(
checkHeaders,
isAuthenticated,
(req, res) => {
- const objectPathname = `../${req.params.tribe}/objects/options/${req.params.optionname}_${req.header.xlang}.json`;
+ const objectPathname = `../${req.params.tribe}/objects/options/${req.params.optionname}_${req.session.header.xlang}.json`;
if (fs.existsSync(objectPathname)) {
res.status(200).json({
status: 200,
@@ -293,14 +293,24 @@ router.post(
"/itm/:tribe/:objectname",
checkHeaders,
isAuthenticated,
- (req, res) => {
+ async (req, res) => {
// Create an item of an object with no specificities
// if specificities then create a route / model that import odmdb
const objectPathname = `../${req.params.tribe}/objects/${req.params.objectname}`;
+ const prepared = await Odmdb.prepareUploadedImages(
+ objectPathname,
+ req.body
+ );
+ if (prepared.status != 200) {
+ return res.status(prepared.status).json(prepared);
+ }
const postitm = Odmdb.cud(objectPathname, "C", req.body, {
xprofils: req.session.header.xprofils,
xalias: req.session.header.xalias,
});
+ if (postitm.status != 200 && prepared.data.written.length > 0) {
+ prepared.data.written.forEach((file) => fs.removeSync(file));
+ }
res.status(postitm.status).json(postitm);
}
);
@@ -336,14 +346,24 @@ router.put(
"/itm/:tribe/:objectname",
checkHeaders,
isAuthenticated,
- (req, res) => {
+ async (req, res) => {
// Create an item of an object with no specificities
// if specificities then create a route / model that import odmdb
const objectPathname = `../${req.params.tribe}/objects/${req.params.objectname}`;
+ const prepared = await Odmdb.prepareUploadedImages(
+ objectPathname,
+ req.body
+ );
+ if (prepared.status != 200) {
+ return res.status(prepared.status).json(prepared);
+ }
const postitm = Odmdb.cud(objectPathname, "U", req.body, {
xprofils: req.session.header.xprofils,
xalias: req.session.header.xalias,
});
+ if (postitm.status != 200 && prepared.data.written.length > 0) {
+ prepared.data.written.forEach((file) => fs.removeSync(file));
+ }
res.status(postitm.status).json(postitm);
}
);
@@ -761,7 +781,7 @@ router.put(
isAuthenticated,
(req, res) => {
const fullpath = path.resolve(
- `${__dirname}/tribes/${req.session.header.xworkon}/schema/${req.params.pathobjectname}.json`
+ `${__dirname}/tribes/${req.session.header.xworkon}/schema/${req.params.objectname}.json`
);
const set = Odmdb.setObject(
path.resolve(`${__dirname}/tribes/${req.session.header.xworkon}`)
diff --git a/apxtri/routes/pagans.js b/apxtri/routes/pagans.js
index 62b52bf..b7d2219 100755
--- a/apxtri/routes/pagans.js
+++ b/apxtri/routes/pagans.js
@@ -142,6 +142,31 @@ router.post("/", checkHeaders, isAuthenticated, async (req, res) => {
});
return;
}
+ if (
+ !/^[a-z0-9]{3,64}$/.test(req.body.alias || "") ||
+ req.body.alias !== req.session.header.xalias
+ ) {
+ res.status(400).json({
+ status: 400,
+ ref: "Pagans",
+ msg: "aliasinvalid",
+ data: { alias: req.body.alias, xalias: req.session.header.xalias },
+ });
+ return;
+ }
+ const allowedtribes = [
+ "adminapi",
+ ...(Array.isArray(req.app.locals.tribeids) ? req.app.locals.tribeids : []),
+ ];
+ if (!allowedtribes.includes(req.body.trustedtribe)) {
+ res.status(404).json({
+ status: 404,
+ ref: "Pagans",
+ msg: "tribeiddoesnotexist",
+ data: { tribe: req.body.trustedtribe },
+ });
+ return;
+ }
const objpagan = { alias: req.body.alias, publickey: req.body.publickey };
console.log(path.resolve(`../adminapi/objects/pagans`));
const newpagan = Odmdb.cud(`../adminapi/objects/pagans`, "C", objpagan, role);
@@ -219,6 +244,14 @@ router.post("/", checkHeaders, isAuthenticated, async (req, res) => {
* Delete an alias and his publickey, this mean that publickey disapear as well as alias. We set dt_delete
* */
router.delete("/alias/:alias", checkHeaders, isAuthenticated, (req, res) => {
+ if (req.params.alias !== req.session.header.xalias) {
+ return res.status(403).json({
+ status: 403,
+ ref: "Pagans",
+ msg: "profilnotallow",
+ data: { alias: req.params.alias, xalias: req.session.header.xalias },
+ });
+ }
const personpath = `../adminapi/objects/pagans`;
const role = {
xalias: req.session.header.xalias,
@@ -263,6 +296,17 @@ router.delete(
checkHeaders,
isAuthenticated,
(req, res) => {
+ const privileged =
+ req.session.header.xprofils.includes("druid") ||
+ req.session.header.xprofils.includes("mayor");
+ if (req.params.alias !== req.session.header.xalias && !privileged) {
+ return res.status(403).json({
+ status: 403,
+ ref: "Pagans",
+ msg: "profilnotallow",
+ data: { alias: req.params.alias, xalias: req.session.header.xalias },
+ });
+ }
const personpath = `../${req.params.tribe}/objects/persons`;
const role = {
xalias: req.session.header.xalias,
@@ -331,8 +375,30 @@ router.get("/person/:alias", checkHeaders, isAuthenticated, (req, res) => {
* @apiBody {string} [addprofils] must be alone no other Body field
* @apiBody {string} [removeprofils] must be alone
*/
-router.put("/person/:tribe", checkHeaders, isAuthenticated, (req, res) => {
+router.put("/person/:tribe", checkHeaders, isAuthenticated, async (req, res) => {
//console.log(req.body);
+ if (!req.body || !/^[a-z0-9]{3,64}$/.test(req.body.alias || "")) {
+ return res.status(400).json({
+ status: 400,
+ ref: "Pagans",
+ msg: "aliasinvalid",
+ data: { alias: req.body && req.body.alias },
+ });
+ }
+ const privileged =
+ req.session.header.xprofils.includes("druid") ||
+ req.session.header.xprofils.includes("mayor");
+ if (
+ req.body.alias !== req.session.header.xalias &&
+ !privileged
+ ) {
+ return res.status(403).json({
+ status: 403,
+ ref: "Pagans",
+ msg: "profilnotallow",
+ data: { alias: req.body.alias, xalias: req.session.header.xalias },
+ });
+ }
const pathobj = `../${req.params.tribe}/objects/persons`;
const action = fs.existsSync(`${pathobj}/itm/${req.body.alias}.json`)
? "U"
@@ -386,10 +452,17 @@ router.put("/person/:tribe", checkHeaders, isAuthenticated, (req, res) => {
if (!req.body.profils) {
req.body.profils = ["anonymous", "pagans", "persons"];
}
+ const prepared = await Odmdb.prepareUploadedImages(pathobj, req.body);
+ if (prepared.status != 200) {
+ return res.status(prepared.status).json(prepared);
+ }
const personup = Odmdb.cud(pathobj, action, req.body, {
xprofils: req.session.header.xprofils,
xalias: req.session.header.xalias,
});
+ if (personup.status != 200 && prepared.data.written.length > 0) {
+ prepared.data.written.forEach((file) => fs.removeSync(file));
+ }
if (log) console.log(currentmod, " personupdate or create:", personup);
res.status(personup.status).json(personup);
}
@@ -415,6 +488,18 @@ router.put("/person/:tribe", checkHeaders, isAuthenticated, (req, res) => {
*
*/
router.post("/keyrecovery", checkHeaders, async (req, res) => {
+ const allowedtribes = [
+ "adminapi",
+ ...(Array.isArray(req.app.locals.tribeids) ? req.app.locals.tribeids : []),
+ ];
+ if (!allowedtribes.includes(req.body.tribe)) {
+ return res.status(404).json({
+ status: 404,
+ ref: "Pagans",
+ msg: "tribeiddoesnotexist",
+ data: { tribe: req.body.tribe },
+ });
+ }
try {
const recores = await Pagans.keyrecovery(
req.body.emailalias,
diff --git a/apxtri/routes/trackings.js b/apxtri/routes/trackings.js
index c18de50..6d07ea8 100644
--- a/apxtri/routes/trackings.js
+++ b/apxtri/routes/trackings.js
@@ -33,6 +33,27 @@ const router = express.Router();
*/
router.post("/newdevice", checkHeaders, async (req, res) => {
if (log) console.log(currentmod, "post new device with", req.body);
+ const xuuid = req.session.header.xuuid;
+ if (
+ !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
+ xuuid
+ )
+ ) {
+ return res.status(400).json({
+ status: 400,
+ ref: "Trackings",
+ msg: "xuuidinvalid",
+ data: { xuuid },
+ });
+ }
+ if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
+ return res.status(400).json({
+ status: 400,
+ ref: "Trackings",
+ msg: "bodyinvalid",
+ data: {},
+ });
+ }
const newdevice = { status: 200, ref: "Trackings", msg: "success", data: {} };
const role = {
xalias: req.session.header.xalias,
@@ -57,9 +78,9 @@ router.post("/newdevice", checkHeaders, async (req, res) => {
confdevice
);
}
- const devicepath = `../${req.session.header.xtribe}/objects/devices/itm/${req.session.header.xuuid}.json`;
+ const devicepath = `../${req.session.header.xtribe}/objects/devices/itm/${xuuid}.json`;
const device = req.body;
- device.xuuid = req.session.header.xuuid;
+ device.xuuid = xuuid;
if (req.session.header.xalias != "anonymous") {
device.alias = req.session.header.xalias;
}
diff --git a/apxtri/routes/tribes.js b/apxtri/routes/tribes.js
index 6fdb367..df95961 100755
--- a/apxtri/routes/tribes.js
+++ b/apxtri/routes/tribes.js
@@ -11,6 +11,32 @@ const checkHeaders = require( '../middlewares/checkHeaders' );
const isAuthenticated = require( '../middlewares/isAuthenticated' );
const router = express.Router();
+const disabledroutes = [
+ '/config',
+ '/www',
+ '/actionanonyme',
+ '/action',
+ '/clientconf',
+ '/spaceweb',
+ '/plugins',
+ '/dirls',
+ '/ls',
+ '/sendjson',
+ '/downloadls',
+ '/upfilepond',
+ '/file',
+ '/uploadfile',
+ '/uploadzip',
+ '/upload',
+];
+router.use( ( req, res, next ) => {
+ if( disabledroutes.some( p => req.path === p || req.path.startsWith( `${p}/` ) ) ) {
+ return res.status( 410 )
+ .json( { status: 410, ref: 'Tribes', msg: 'routedisabled' } );
+ }
+ next();
+} );
+
/**
* @api {get} adminapi/tribes/conf/:tribe - tribe list
diff --git a/apxtri/routes/wwws.js b/apxtri/routes/wwws.js
index 0ad71b6..30ce556 100644
--- a/apxtri/routes/wwws.js
+++ b/apxtri/routes/wwws.js
@@ -53,10 +53,32 @@ router.get("/getwco/:wconame", (req, res) => {
missquery+=q+" "
}
})
+ const namepattern = /^[A-Za-z0-9_.:-]{1,64}$/;
+ const allowedtribes = [
+ "adminapi",
+ ...(Array.isArray(req.app.locals.tribeids) ? req.app.locals.tribeids : []),
+ ];
+ const wconame = req.params.wconame.replace(/\.js$/, "");
+ const tagids = req.query.tagid ? String(req.query.tagid).split(",") : [];
+ const invalid =
+ !namepattern.test(wconame) ||
+ !allowedtribes.includes(req.query.tribe) ||
+ !allowedtribes.includes(req.query.wcotribe) ||
+ !namepattern.test(req.query.xapp || "") ||
+ !namepattern.test(req.query.pagename || "") ||
+ String(req.query.code || "").length > 128 ||
+ tagids.some((tagid) => !namepattern.test(tagid));
if (missquery!=""){
res.status(403).json({status:403,ref:"Wwws",msg:"missquery",data:{missquery}})
+ }else if (invalid) {
+ res.status(400).json({
+ status: 400,
+ ref: "Wwws",
+ msg: "queryinvalid",
+ data: { wconame, query: req.query },
+ });
}else{
- const getwco=Wwws.getwco(req.params.wconame.replace(/\.js$/,''),req.query)
+ const getwco=Wwws.getwco(wconame,req.query)
if (getwco.status==200){
res.set('Content-Type', 'application/javascript');
res.sendFile(getwco.data.file)
diff --git a/apxtri/setup/cleanupimages.js b/apxtri/setup/cleanupimages.js
new file mode 100644
index 0000000..eadab76
--- /dev/null
+++ b/apxtri/setup/cleanupimages.js
@@ -0,0 +1,137 @@
+#!/usr/bin/env node
+const fs = require("fs-extra");
+const path = require("path");
+const glob = require("glob");
+
+const args = process.argv.slice(2);
+const deletefiles = args.includes("--delete");
+const quarantine = args.includes("--quarantine");
+const dryrun = !deletefiles && !quarantine;
+const adminapiroot = path.resolve(__dirname, "../..");
+const townroot = path.resolve(adminapiroot, "..");
+const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
+const quarantinedir = path.join(
+ adminapiroot,
+ "tmp",
+ "quarantine",
+ timestamp
+);
+const imageext = [".webp", ".png", ".jpg", ".jpeg", ".gif"];
+const imagelabel = /_(imgavatar|backgroundimage|companylogo)\./i;
+
+const readhead = (file) => {
+ const fd = fs.openSync(file, "r");
+ const buffer = Buffer.alloc(12);
+ fs.readSync(fd, buffer, 0, 12, 0);
+ fs.closeSync(fd);
+ return buffer;
+};
+
+const validmagic = (buffer) => {
+ if (buffer.length < 12) return false;
+ const ispng =
+ buffer[0] === 0x89 &&
+ buffer[1] === 0x50 &&
+ buffer[2] === 0x4e &&
+ buffer[3] === 0x47 &&
+ buffer[4] === 0x0d &&
+ buffer[5] === 0x0a &&
+ buffer[6] === 0x1a &&
+ buffer[7] === 0x0a;
+ const isjpeg =
+ buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
+ const gif = buffer.slice(0, 6).toString();
+ const isgif = gif === "GIF87a" || gif === "GIF89a";
+ const iswebp =
+ buffer.slice(0, 4).toString() === "RIFF" &&
+ buffer.slice(8, 12).toString() === "WEBP";
+ return ispng || isjpeg || isgif || iswebp;
+};
+
+const objectdirs = [];
+glob
+ .sync(`${townroot}/*/objects`, { nodir: false, follow: false })
+ .forEach((dir) => {
+ if (!objectdirs.includes(dir)) objectdirs.push(dir);
+ });
+const adminobjects = path.join(adminapiroot, "objects");
+if (fs.existsSync(adminobjects) && !objectdirs.includes(adminobjects)) {
+ objectdirs.push(adminobjects);
+}
+
+const flagged = [];
+objectdirs.forEach((objectdir) => {
+ if (!fs.existsSync(objectdir)) return;
+ glob
+ .sync("**/*", { cwd: objectdir, nodir: true, dot: false, follow: false })
+ .forEach((rel) => {
+ if (/(^|\/)(node_modules|tmp|logs|\.git)(\/|$)/.test(rel)) return;
+ const file = path.join(objectdir, rel);
+ let stat;
+ try {
+ stat = fs.lstatSync(file);
+ } catch (err) {
+ return;
+ }
+ if (!stat.isFile() || stat.isSymbolicLink()) return;
+ const base = path.basename(file);
+ const dirparts = rel.split(path.sep);
+ const top = dirparts[0];
+ const inobjectimg = dirparts.length >= 3 && dirparts[1] === "img";
+ const ext = path.extname(base).toLowerCase();
+ let reason = "";
+ if (base.startsWith("zz_")) {
+ reason = "zzartifact";
+ } else if (["wwws", "apidoc"].includes(top)) {
+ return;
+ } else if (!inobjectimg && imagelabel.test(base)) {
+ reason = "imageoutsideimg";
+ } else if (inobjectimg && !imageext.includes(ext)) {
+ reason = "extensionnotallowed";
+ } else if (
+ inobjectimg &&
+ imageext.includes(ext) &&
+ !validmagic(readhead(file))
+ ) {
+ reason = "invalidmagic";
+ }
+ if (reason) {
+ flagged.push({ file, reason, rel, objectdir });
+ }
+ });
+});
+
+if (flagged.length == 0) {
+ console.log("No suspicious image found.");
+ process.exit(0);
+}
+
+if (!dryrun) fs.ensureDirSync(quarantinedir);
+
+flagged.forEach((item) => {
+ console.log(
+ `${dryrun ? "WOULD QUARANTINE" : deletefiles ? "DELETE" : "QUARANTINE"} [${item.reason}] ${item.file}`
+ );
+ if (dryrun) return;
+ if (deletefiles) {
+ fs.removeSync(item.file);
+ return;
+ }
+ const dest = path.resolve(
+ quarantinedir,
+ path.relative(townroot, item.file)
+ );
+ if (!dest.startsWith(`${quarantinedir}${path.sep}`)) {
+ console.log(`skip unsafe quarantine path: ${dest}`);
+ return;
+ }
+ fs.moveSync(item.file, dest, { overwrite: true });
+});
+
+if (!dryrun) {
+ fs.ensureDirSync(quarantinedir);
+ fs.outputJSONSync(path.join(quarantinedir, "report.json"), flagged, {
+ spaces: 2,
+ });
+ console.log(`Report written to ${path.join(quarantinedir, "report.json")}`);
+}
diff --git a/apxtri/setup/initadminapi.json b/apxtri/setup/initadminapi.json
index 21c8041..9953351 100644
--- a/apxtri/setup/initadminapi.json
+++ b/apxtri/setup/initadminapi.json
@@ -27,7 +27,7 @@
"xtrkversion"
],
"appset": {
- "trust proxy": true
+ "trust proxy": 1
},
"json": {
"limit": "10mb",
@@ -49,6 +49,7 @@
"worker_connections": 1024
},
"emailcontact": "",
+ "emailalertsecu": "",
"smtp": {},
"sms": {}
}
\ No newline at end of file
diff --git a/apxtri/setup/nginx.wwwscf b/apxtri/setup/nginx.wwwscf
index 2a67dda..9cc7782 100644
--- a/apxtri/setup/nginx.wwwscf
+++ b/apxtri/setup/nginx.wwwscf
@@ -56,6 +56,8 @@ location /api/ {
proxy_pass http://localhost:{{{api.port}}};
proxy_redirect off;
include proxy_params;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $remote_addr;
}
location /socket/ {
proxy_pass http://127.0.0.1:{{socket.port}};
diff --git a/schema/persons.json b/schema/persons.json
index 5d8bb59..1a09dd6 100644
--- a/schema/persons.json
+++ b/schema/persons.json
@@ -9,6 +9,7 @@
"title": "alias",
"description": "A unique string identifying a unique public key",
"type": "string",
+ "pattern": "^[a-z0-9]{3,64}$",
"options": {
"$ref": "adminapi/objects/pagans/idx/lst_alias"
}
@@ -17,6 +18,7 @@
"title": "Owner of this person",
"description": "For accessright purpose this is always equal as alias",
"type": "string",
+ "pattern": "^[a-z0-9]{3,64}$",
"options": {
"$ref": "adminapi/objects/pagans/idx/lst_alias"
}