add middleware security to prevent injection

This commit is contained in:
2026-11-08 14:24:30 +01:00
parent 4bfcf28de9
commit 57df038a76
20 changed files with 1117 additions and 95 deletions
+147 -34
View File
@@ -43,6 +43,14 @@ SEE README.md to start
********************************************/ ********************************************/
const apxtri = {}; 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 () => { apxtri.main = async () => {
if (process.env.NODE_MODE !== 'dev' && !fs.existsSync("/etc/nginx/nginx.conf")) { if (process.env.NODE_MODE !== 'dev' && !fs.existsSync("/etc/nginx/nginx.conf")) {
console.log( console.log(
@@ -51,39 +59,97 @@ apxtri.main = async () => {
process.exit(0); process.exit(0);
} }
//Check prerequest data //Check prerequest data
if ( const adminapipath = "../adminapi/objects/tribes/itm/adminapi.json";
fs.existsSync("../adminapi/objects/tribes/idx/tribes_dns.json") && const tribednspath = "../adminapi/objects/tribes/idx/tribes_dns.json";
fs.existsSync("../adminapi/objects/tribes/itm/adminapi.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.
// check all tribes are in tribes_dns.json if (fs.existsSync(adminapipath)) {
const conf = fs.readJSONSync( // check all tribes are in tribes_dns.json (rebuild it if needed)
"../adminapi/objects/tribes/itm/adminapi.json" 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( if (!conf.api) {
`../adminapi/objects/tribes/idx/tribes_dns.json` conf.api = JSON.parse(JSON.stringify(initconf.api));
); console.log(
//check if new tribe was add "\x1b[31m adminapi.json is missing 'api', using defaults from apxtri/setup/initadminapi.json. Restore this file from a backup.\x1b[0m"
glob );
}
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("../*") .sync("../*")
.filter((f) => fs.lstatSync(f).isDirectory()) .filter((f) => fs.lstatSync(f).isDirectory())
.forEach(async (t) => { .filter((f) => fs.existsSync(`${f}/apxtri/routes`));
const tribe = path.basename(t); for (const t of tribedirs) {
//console.log(tribe); const tribe = path.basename(t);
if (![".", ".."].includes(tribe) && !tribesdns[tribe]) { if ([".", ".."].includes(tribe) || tribesdns[tribe]) continue;
await apxtri.setuptribe(tribe, conf); // 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 { } else {
const initconf = fs.readJsonSync( const initconf = fs.readJsonSync(
"../adminapi/apxtri/setup/initadminapi.json" "../adminapi/apxtri/setup/initadminapi.json"
); );
await apxtri.setuptribe("adminapi", initconf); 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 () => { apxtri.getip = async () => {
/** /**
@@ -112,6 +178,21 @@ apxtri.getip = async () => {
return network; return network;
}; };
apxtri.setuptribe = async (tribe, conf) => { 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; let inittribe;
if (tribe == "adminapi") { if (tribe == "adminapi") {
console.log( console.log(
@@ -131,9 +212,13 @@ apxtri.setuptribe = async (tribe, conf) => {
} }
} else { } else {
console.log(`a new tribe called ${tribe} was detected`); console.log(`a new tribe called ${tribe} was detected`);
const adminapiconf = fs.readJSONSync( const initconf = fs.readJsonSync(
"../adminapi/objects/tribes/itm/adminapi.json" "../adminapi/apxtri/setup/initadminapi.json"
); );
const adminapiconf =
conf && conf.api && conf.socket
? conf
: fs.readJSONSync("../adminapi/objects/tribes/itm/adminapi.json");
inittribe = { inittribe = {
tribeId: tribe, tribeId: tribe,
townpath: __dirname.replace("/adminapi/apxtri", ""), townpath: __dirname.replace("/adminapi/apxtri", ""),
@@ -142,8 +227,8 @@ apxtri.setuptribe = async (tribe, conf) => {
nationId: conf.nationId, nationId: conf.nationId,
townId: conf.townId, townId: conf.townId,
activelog: [], activelog: [],
api: { port: adminapiconf.api.port }, api: { port: (adminapiconf.api || initconf.api).port },
socket: { port: adminapiconf.socket.port }, socket: { port: (adminapiconf.socket || initconf.socket).port },
}; };
} }
inittribe.sudoUser = process.env.USER; inittribe.sudoUser = process.env.USER;
@@ -171,7 +256,7 @@ apxtri.setuptribe = async (tribe, conf) => {
let lsttribe = fs.existsSync(lst_tribeIdpath) let lsttribe = fs.existsSync(lst_tribeIdpath)
? fs.readJsonSync(lst_tribeIdpath) ? fs.readJsonSync(lst_tribeIdpath)
: []; : [];
lsttribe.push(tribe); if (!lsttribe.includes(tribe)) lsttribe.push(tribe);
fs.outputJSONSync(lst_tribeIdpath, lsttribe); fs.outputJSONSync(lst_tribeIdpath, lsttribe);
const tribes_dnspath = "../adminapi/objects/tribes/idx/tribes_dns.json"; const tribes_dnspath = "../adminapi/objects/tribes/idx/tribes_dns.json";
const tribedns = fs.existsSync(tribes_dnspath) const tribedns = fs.existsSync(tribes_dnspath)
@@ -248,7 +333,12 @@ apxtri.setuptribe = async (tribe, conf) => {
apxtri.runexpress = async (tribesdns, conf) => { apxtri.runexpress = async (tribesdns, conf) => {
const Odmdb = require(path.resolve("./apxtri/models/Odmdb.js")); 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:[]}]} // context is store in /itm/tribename.json ={contexte:{routes:[],models:[{model:,tplstringslg:[]}]}
// routes={url,route} check how to add plugin tribe route later // 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) // 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); if (!doms.includes(dm)) doms.push(dm);
//reindex database attention check dev-ants/.. a bug was fixed //reindex database attention check dev-ants/.. a bug was fixed
glob.sync(`../${t}/objects/*`).forEach((o) => { 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); console.log("reindex: ", o);
Odmdb.runidx(o); Odmdb.runidx(o);
}); });
@@ -292,18 +389,34 @@ apxtri.runexpress = async (tribesdns, conf) => {
}); });
const app = express(); const app = express();
// load express parameter from conf // load express parameter from conf
Object.keys(conf.api.appset).forEach((p) => { const apiconf = conf.api || {};
app.set(p, conf.api.appset[p]); 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 // To set depending of data form or get size to send
tribeIds.forEach((t) => { tribeIds.forEach((t) => {
app.use(`/${t}`, bodyParser.urlencoded(conf.api.bodyparse.urlencoded)); app.use(
app.use(`/${t}`, bodyParser.json(conf.api.bodyparse.json)); `/${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 // 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.disable("x-powered-by"); // for security
app.locals.tribeids = tribeIds; app.locals.tribeids = tribeIds;
app.use(checkRequestSafety);
const currentmod = "apxtri"; const currentmod = "apxtri";
const log = process.env.ACTIVELOG ? process.env.ACTIVELOG.split(',').includes(currentmod) : false; const log = process.env.ACTIVELOG ? process.env.ACTIVELOG.split(',').includes(currentmod) : false;
console.log( console.log(
+117 -5
View File
@@ -60,6 +60,14 @@ const log = process.env.ACTIVELOG ? process.env.ACTIVELOG.split(',').includes(cu
* data: {xalias} * 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) => { const checkHeaders = (req, res, next) => {
req.session = {}; req.session = {};
const header = {}; const header = {};
@@ -90,14 +98,118 @@ const checkHeaders = (req, res, next) => {
data: {missingheader}, 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 ) //console.log( req.app.locals.tribeids )
// xtribe == "town" is used during the setup process // xtribe == "town" is used during the setup process
// xtribe == "adminapi" is used to access /adminapi // xtribe == "adminapi" is used to access /adminapi
if ( if (!allowedtribes.includes(header.xtribe)) {
!(
["town","adminapi"].includes(header.xtribe) || req.app.locals.tribeids.includes(header.xtribe)
)
) {
return res.status(404).json({ return res.status(404).json({
status:404, status:404,
ref: "middlewares", ref: "middlewares",
+67
View File
@@ -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;
+24 -4
View File
@@ -117,13 +117,33 @@ const isAuthenticated = async (req, res, next) => {
if (log) console.log(currentmod,"time out penalty"); 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)) { if (!fs.existsSync(tmpfs)) {
// need to check detached sign // need to check detached sign
let publickey = ""; let publickey = "";
const aliasinfo = `../adminapi/objects/pagans/itm/${req.session.header.xalias}.json`; if (paganexists) {
if (fs.existsSync(aliasinfo)) { publickey = fs.readJsonSync(paganpath).publickey;
publickey = fs.readJsonSync(aliasinfo).publickey; } else if (iscreatepagan && req.body.publickey) {
} else if (req.body.publickey) {
resnotauth.data.xaliasexists = false; resnotauth.data.xaliasexists = false;
publickey = req.body.publickey; publickey = req.body.publickey;
} }
+2 -1
View File
@@ -106,7 +106,8 @@ Checkjson.schema.properties.format = {
ipv4: /^([0–9]{1,3}.){3}.([0–9]{1,3})$/, 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}:))$/, 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}$/, 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,}/, telephoneinter: /^\+*(\d{3})*[0-9,\-]{8,}/,
password: password:
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&.])[A-Za-z\d$@$!%*?&.{}:|\s]{8,}/, /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&.])[A-Za-z\d$@$!%*?&.{}:|\s]{8,}/,
+12 -3
View File
@@ -21,7 +21,18 @@ const log = process.env.ACTIVELOG
const Notifications = {}; const Notifications = {};
Notifications.get = (alias, tribeId) => { 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) : {}; const msg = fs.existsSync(notiffile) ? fs.readJSONSync(notiffile) : {};
return { return {
status: 200, 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 { return {
status: 200, status: 200,
ref: "Notifications", ref: "Notifications",
+171 -36
View File
@@ -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) => { Odmdb.cud = (objectPathname, crud, itm, role, runindex = true) => {
const getschema = Odmdb.Schema(objectPathname, true); const getschema = Odmdb.Schema(objectPathname, true);
if (getschema.status != 200) return getschema; if (getschema.status != 200) return getschema;
@@ -637,6 +776,17 @@ Odmdb.cud = (objectPathname, crud, itm, role, runindex = true) => {
data: { missingkey: getschema.data.schema.apxid }, 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}/itm/`);
fs.ensureDirSync(`${objectPathname}/idx/`); fs.ensureDirSync(`${objectPathname}/idx/`);
const existid = fs.existsSync( const existid = fs.existsSync(
@@ -774,40 +924,6 @@ Odmdb.cud = (objectPathname, crud, itm, role, runindex = true) => {
//console.log(k, itmtostore[k], itmtostore[k] === ""); //console.log(k, itmtostore[k], itmtostore[k] === "");
if (itmtostore[k] === "") delete 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 // check consistency of datatostore
//console.log("itmstostore::::::", itmtostore); //console.log("itmstostore::::::", itmtostore);
const chkdata = Checkjson.schema.data( const chkdata = Checkjson.schema.data(
@@ -858,7 +974,18 @@ Odmdb.cud = (objectPathname, crud, itm, role, runindex = true) => {
* *
*/ */
Odmdb.runidx = (objectPathname, schema) => { 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) { if (!schema || !schema.apxid) {
const getschema = Odmdb.Schema(objectPathname, true); const getschema = Odmdb.Schema(objectPathname, true);
if (getschema.status != 200) { if (getschema.status != 200) {
@@ -867,6 +994,14 @@ Odmdb.runidx = (objectPathname, schema) => {
} }
schema = getschema.data.schema; schema = getschema.data.schema;
} }
if (!Array.isArray(schema.apxidx)) {
return {
status: 406,
ref: "Odmdb",
msg: "apxidxmissing",
data: { objectPathname },
};
}
const ventil = {}; const ventil = {};
schema.apxidx.forEach((id) => { schema.apxidx.forEach((id) => {
ventil[id.name] = id; ventil[id.name] = id;
@@ -881,7 +1016,7 @@ Odmdb.runidx = (objectPathname, schema) => {
} else { } else {
if (!schema.properties[id.keyval]){ if (!schema.properties[id.keyval]){
console.log(`Check ${objectPathname} index ${id.name} property ${id.keyval} does not exist`) 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].isunique = schema.apxuniquekey.includes(id.keyval);
ventil[id.name].nbobjkey = id.objkey ? id.objkey.length : 0; ventil[id.name].nbobjkey = id.objkey ? id.objkey.length : 0;
+22
View File
@@ -84,6 +84,20 @@ Pagans.keyrecovery = async (emailalias, tribe, search, lg) => {
data: { 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 = []; let emaillist = [];
if (emailalias == "email") { if (emailalias == "email") {
search = search.toLowerCase(); search = search.toLowerCase();
@@ -148,6 +162,14 @@ Pagans.keyrecovery = async (emailalias, tribe, search, lg) => {
* *
**/ **/
Pagans.getalias = (alias) => { 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 //bypass Odmdb cause all is public save ressources
console.log(path.resolve(`../adminapi/objects/pagans/itm/${alias}.json`)); console.log(path.resolve(`../adminapi/objects/pagans/itm/${alias}.json`));
if (fs.existsSync(`../adminapi/objects/pagans/itm/${alias}.json`)) { if (fs.existsSync(`../adminapi/objects/pagans/itm/${alias}.json`)) {
+139
View File
@@ -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: `<pre>${JSON.stringify(entry, null, 2)}</pre>`,
},
"adminapi"
).catch((err) => {
console.log("ALERT security email failed", err.message);
});
};
module.exports = Security;
+34
View File
@@ -16,6 +16,24 @@ const log = process.env.ACTIVELOG ? process.env.ACTIVELOG.split(',').includes(cu
const Wwws = {}; const Wwws = {};
Wwws.getwco = (wconame, ctx) => { 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 * In a webpage to add a wco
* <script src="/api/adminapi/wwws/getwco/wco.js?wcotribe=adminapi&tribe=adminapi&xapp=admin&pagename=apxid&code=enjoy&tagid=id"></script> * <script src="/api/adminapi/wwws/getwco/wco.js?wcotribe=adminapi&tribe=adminapi&xapp=admin&pagename=apxid&code=enjoy&tagid=id"></script>
@@ -486,6 +504,22 @@ Wwws.initlocalwco = (tribwco, profils, lg) => {
}; };
Wwws.initlocaldata = (tribe, appname, pagename, version, 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`; const fileparam = `../${tribe}/objects/wwws/itm/${appname}.json`;
//console.log(path.resolve(fileparam)); //console.log(path.resolve(fileparam));
if (!fs.existsSync(fileparam)) { if (!fs.existsSync(fileparam)) {
+57 -3
View File
@@ -1,9 +1,13 @@
const express = require("express"); const express = require("express");
const crypto = require("crypto");
const fs = require("fs-extra"); const fs = require("fs-extra");
const path = require("path"); const path = require("path");
// Classes // Classes
const Notifications = require("../models/Notifications.js"); const Notifications = require("../models/Notifications.js");
const emailpattern =
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
// Middlewares // Middlewares
const checkHeaders = require("../middlewares/checkHeaders"); const checkHeaders = require("../middlewares/checkHeaders");
const isAuthenticated = require("../middlewares/isAuthenticated"); const isAuthenticated = require("../middlewares/isAuthenticated");
@@ -26,7 +30,7 @@ const router = express.Router();
**/ **/
router.get("/messages/:alias/:tribeId", (req, res) => { router.get("/messages/:alias/:tribeId", (req, res) => {
const getnot = Notifications.get(req.params.alias, req.params.tribeId); 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 }, 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 const key = req.body.contactpoint
? req.body.contactpoint ? req.body.contactpoint
: req.body[req.body.typekey]; : req.body[req.body.typekey];
@@ -201,7 +233,18 @@ router.get(
* *
*/ */
router.get("/stat/maillinglst/:tribe", checkHeaders, async (req, res) => { 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); const resstat = Notifications.statmaillist(req.params.tribe);
res.status(resstat.status).json(resstat); 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) => { router.get("/checkemail/:email/:digit6", checkHeaders, async (req, res) => {
const { email, digit6 } = req.params; 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 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") { if (digit6 === "0") {
const code = email.includes("smatchit.io") ? "789000" : Math.floor(100000 + Math.random() * 900000).toString(); const code = email.includes("smatchit.io") ? "789000" : Math.floor(100000 + Math.random() * 900000).toString();
+24 -4
View File
@@ -82,7 +82,7 @@ router.get(
checkHeaders, checkHeaders,
isAuthenticated, isAuthenticated,
(req, res) => { (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)) { if (fs.existsSync(objectPathname)) {
res.status(200).json({ res.status(200).json({
status: 200, status: 200,
@@ -293,14 +293,24 @@ router.post(
"/itm/:tribe/:objectname", "/itm/:tribe/:objectname",
checkHeaders, checkHeaders,
isAuthenticated, isAuthenticated,
(req, res) => { async (req, res) => {
// Create an item of an object with no specificities // Create an item of an object with no specificities
// if specificities then create a route / model that import odmdb // if specificities then create a route / model that import odmdb
const objectPathname = `../${req.params.tribe}/objects/${req.params.objectname}`; 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, { const postitm = Odmdb.cud(objectPathname, "C", req.body, {
xprofils: req.session.header.xprofils, xprofils: req.session.header.xprofils,
xalias: req.session.header.xalias, 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); res.status(postitm.status).json(postitm);
} }
); );
@@ -336,14 +346,24 @@ router.put(
"/itm/:tribe/:objectname", "/itm/:tribe/:objectname",
checkHeaders, checkHeaders,
isAuthenticated, isAuthenticated,
(req, res) => { async (req, res) => {
// Create an item of an object with no specificities // Create an item of an object with no specificities
// if specificities then create a route / model that import odmdb // if specificities then create a route / model that import odmdb
const objectPathname = `../${req.params.tribe}/objects/${req.params.objectname}`; 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, { const postitm = Odmdb.cud(objectPathname, "U", req.body, {
xprofils: req.session.header.xprofils, xprofils: req.session.header.xprofils,
xalias: req.session.header.xalias, 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); res.status(postitm.status).json(postitm);
} }
); );
@@ -761,7 +781,7 @@ router.put(
isAuthenticated, isAuthenticated,
(req, res) => { (req, res) => {
const fullpath = path.resolve( 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( const set = Odmdb.setObject(
path.resolve(`${__dirname}/tribes/${req.session.header.xworkon}`) path.resolve(`${__dirname}/tribes/${req.session.header.xworkon}`)
+86 -1
View File
@@ -142,6 +142,31 @@ router.post("/", checkHeaders, isAuthenticated, async (req, res) => {
}); });
return; 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 }; const objpagan = { alias: req.body.alias, publickey: req.body.publickey };
console.log(path.resolve(`../adminapi/objects/pagans`)); console.log(path.resolve(`../adminapi/objects/pagans`));
const newpagan = Odmdb.cud(`../adminapi/objects/pagans`, "C", objpagan, role); 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 * 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) => { 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 personpath = `../adminapi/objects/pagans`;
const role = { const role = {
xalias: req.session.header.xalias, xalias: req.session.header.xalias,
@@ -263,6 +296,17 @@ router.delete(
checkHeaders, checkHeaders,
isAuthenticated, isAuthenticated,
(req, res) => { (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 personpath = `../${req.params.tribe}/objects/persons`;
const role = { const role = {
xalias: req.session.header.xalias, 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} [addprofils] must be alone no other Body field
* @apiBody {string} [removeprofils] must be alone * @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); //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 pathobj = `../${req.params.tribe}/objects/persons`;
const action = fs.existsSync(`${pathobj}/itm/${req.body.alias}.json`) const action = fs.existsSync(`${pathobj}/itm/${req.body.alias}.json`)
? "U" ? "U"
@@ -386,10 +452,17 @@ router.put("/person/:tribe", checkHeaders, isAuthenticated, (req, res) => {
if (!req.body.profils) { if (!req.body.profils) {
req.body.profils = ["anonymous", "pagans", "persons"]; 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, { const personup = Odmdb.cud(pathobj, action, req.body, {
xprofils: req.session.header.xprofils, xprofils: req.session.header.xprofils,
xalias: req.session.header.xalias, 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); if (log) console.log(currentmod, " personupdate or create:", personup);
res.status(personup.status).json(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) => { 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 { try {
const recores = await Pagans.keyrecovery( const recores = await Pagans.keyrecovery(
req.body.emailalias, req.body.emailalias,
+23 -2
View File
@@ -33,6 +33,27 @@ const router = express.Router();
*/ */
router.post("/newdevice", checkHeaders, async (req, res) => { router.post("/newdevice", checkHeaders, async (req, res) => {
if (log) console.log(currentmod, "post new device with", req.body); 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 newdevice = { status: 200, ref: "Trackings", msg: "success", data: {} };
const role = { const role = {
xalias: req.session.header.xalias, xalias: req.session.header.xalias,
@@ -57,9 +78,9 @@ router.post("/newdevice", checkHeaders, async (req, res) => {
confdevice 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; const device = req.body;
device.xuuid = req.session.header.xuuid; device.xuuid = xuuid;
if (req.session.header.xalias != "anonymous") { if (req.session.header.xalias != "anonymous") {
device.alias = req.session.header.xalias; device.alias = req.session.header.xalias;
} }
+26
View File
@@ -11,6 +11,32 @@ const checkHeaders = require( '../middlewares/checkHeaders' );
const isAuthenticated = require( '../middlewares/isAuthenticated' ); const isAuthenticated = require( '../middlewares/isAuthenticated' );
const router = express.Router(); 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 * @api {get} adminapi/tribes/conf/:tribe - tribe list
+23 -1
View File
@@ -53,10 +53,32 @@ router.get("/getwco/:wconame", (req, res) => {
missquery+=q+" " 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!=""){ if (missquery!=""){
res.status(403).json({status:403,ref:"Wwws",msg:"missquery",data:{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{ }else{
const getwco=Wwws.getwco(req.params.wconame.replace(/\.js$/,''),req.query) const getwco=Wwws.getwco(wconame,req.query)
if (getwco.status==200){ if (getwco.status==200){
res.set('Content-Type', 'application/javascript'); res.set('Content-Type', 'application/javascript');
res.sendFile(getwco.data.file) res.sendFile(getwco.data.file)
+137
View File
@@ -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")}`);
}
+2 -1
View File
@@ -27,7 +27,7 @@
"xtrkversion" "xtrkversion"
], ],
"appset": { "appset": {
"trust proxy": true "trust proxy": 1
}, },
"json": { "json": {
"limit": "10mb", "limit": "10mb",
@@ -49,6 +49,7 @@
"worker_connections": 1024 "worker_connections": 1024
}, },
"emailcontact": "", "emailcontact": "",
"emailalertsecu": "",
"smtp": {}, "smtp": {},
"sms": {} "sms": {}
} }
+2
View File
@@ -56,6 +56,8 @@ location /api/ {
proxy_pass http://localhost:{{{api.port}}}; proxy_pass http://localhost:{{{api.port}}};
proxy_redirect off; proxy_redirect off;
include proxy_params; include proxy_params;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
} }
location /socket/ { location /socket/ {
proxy_pass http://127.0.0.1:{{socket.port}}; proxy_pass http://127.0.0.1:{{socket.port}};
+2
View File
@@ -9,6 +9,7 @@
"title": "alias", "title": "alias",
"description": "A unique string identifying a unique public key", "description": "A unique string identifying a unique public key",
"type": "string", "type": "string",
"pattern": "^[a-z0-9]{3,64}$",
"options": { "options": {
"$ref": "adminapi/objects/pagans/idx/lst_alias" "$ref": "adminapi/objects/pagans/idx/lst_alias"
} }
@@ -17,6 +18,7 @@
"title": "Owner of this person", "title": "Owner of this person",
"description": "For accessright purpose this is always equal as alias", "description": "For accessright purpose this is always equal as alias",
"type": "string", "type": "string",
"pattern": "^[a-z0-9]{3,64}$",
"options": { "options": {
"$ref": "adminapi/objects/pagans/idx/lst_alias" "$ref": "adminapi/objects/pagans/idx/lst_alias"
} }