@galadrim/galadmin 0.1.230 → 0.1.233
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +144 -31
- package/dist/api.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/dbManager.js +1 -1
- package/dist/utils/dbManager.js.map +1 -1
- package/package.json +3 -2
- package/src/api.ts +210 -47
- package/src/index.ts +5 -1
- package/src/utils/dbManager.ts +4 -1
package/dist/utils/dbManager.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dbManager.js","sourceRoot":"","sources":["../../src/utils/dbManager.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"dbManager.js","sourceRoot":"","sources":["../../src/utils/dbManager.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@galadrim/galadmin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.233",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
"@google-cloud/storage": "^7.17.2",
|
|
17
17
|
"@rollup/plugin-babel": "^5.3.0",
|
|
18
18
|
"@rollup/plugin-node-resolve": "^13.0.4",
|
|
19
|
+
"@aws-sdk/client-s3": "^3.1114.0",
|
|
20
|
+
"@aws-sdk/s3-request-presigner": "^3.1114.0",
|
|
19
21
|
"argon2": "^0.31.2",
|
|
20
|
-
"aws-sdk": "^2.958.0",
|
|
21
22
|
"axios": "^0.28.1",
|
|
22
23
|
"bcryptjs": "^2.4.3",
|
|
23
24
|
"connect-pg-simple": "^9.0.1",
|
package/src/api.ts
CHANGED
|
@@ -5,7 +5,8 @@ import * as path from "path";
|
|
|
5
5
|
import * as crypto from "crypto";
|
|
6
6
|
import * as mime from "mime";
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
9
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
9
10
|
import { Storage } from "@google-cloud/storage";
|
|
10
11
|
import sizeOf from "image-size";
|
|
11
12
|
import * as serializeJs from "serialize-javascript";
|
|
@@ -76,6 +77,100 @@ export default (db, config) => {
|
|
|
76
77
|
next();
|
|
77
78
|
});
|
|
78
79
|
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// SECURITY: server-side authorization.
|
|
82
|
+
//
|
|
83
|
+
// The config-filtering middleware above only decides which views the *client*
|
|
84
|
+
// is shown, and sets canEdit/canCreate/canDelete flags on that served config.
|
|
85
|
+
// Nothing enforced those decisions server-side, so a session with the lowest
|
|
86
|
+
// role could POST directly to /updateRecord, /deleteRecord, /customQuery, …
|
|
87
|
+
// and act on any view — including read-only ones — as the DB user (root).
|
|
88
|
+
// This block re-checks every sensitive route against the view's declared roles.
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
const ROOT_ROLE = "root";
|
|
91
|
+
const roleOf = (req) => req.session && req.session.user && req.session.user.role;
|
|
92
|
+
// Write access = the view's `roles` (root always allowed).
|
|
93
|
+
const canWriteView = (view, role) =>
|
|
94
|
+
role === ROOT_ROLE || !view || !view.roles || view.roles.includes(role);
|
|
95
|
+
|
|
96
|
+
const viewFromRequest = (req) => {
|
|
97
|
+
const idx =
|
|
98
|
+
req.body && req.body.viewIndex !== undefined
|
|
99
|
+
? req.body.viewIndex
|
|
100
|
+
: req.query && req.query.viewIndex !== undefined
|
|
101
|
+
? req.query.viewIndex
|
|
102
|
+
: undefined;
|
|
103
|
+
if (idx === undefined || !req.config || !req.config.views) return undefined;
|
|
104
|
+
return req.config.views[idx];
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Routes that mutate data and reference their target view via viewIndex
|
|
108
|
+
// (body for POST bodies, query for /uploadFile). Import routes carry the
|
|
109
|
+
// viewIndex inside a multipart body and are guarded inline in their handlers.
|
|
110
|
+
const MUTATING_VIEW_ROUTES = new Set([
|
|
111
|
+
"/updateRecord",
|
|
112
|
+
"/createRecord",
|
|
113
|
+
"/deleteRecord",
|
|
114
|
+
"/deleteRecords",
|
|
115
|
+
"/deleteRecordFromPanel",
|
|
116
|
+
"/customAction",
|
|
117
|
+
"/uploadFile",
|
|
118
|
+
]);
|
|
119
|
+
// /duplicateRecord references its table via req.body.table (not a viewIndex),
|
|
120
|
+
// so it is guarded inline in its handler instead of by the generic middleware.
|
|
121
|
+
|
|
122
|
+
// Column names are interpolated into INSERT/UPDATE statements; only accept plain
|
|
123
|
+
// SQL identifiers so a crafted key cannot break out of the column list.
|
|
124
|
+
const areSafeColumnNames = (data) =>
|
|
125
|
+
!data ||
|
|
126
|
+
Object.keys(data).every((k) => /^[A-Za-z0-9_]+$/.test(k));
|
|
127
|
+
|
|
128
|
+
// Tables the current role is allowed to write, gathered from its writable views
|
|
129
|
+
// and their creatable panels — used to bound /duplicateRecord's target table.
|
|
130
|
+
const writableTables = (req) => {
|
|
131
|
+
const role = roleOf(req);
|
|
132
|
+
const tables = new Set<string>();
|
|
133
|
+
for (const view of req.config?.views || []) {
|
|
134
|
+
if (!canWriteView(view, role)) continue;
|
|
135
|
+
if (view.tableName) tables.add(view.tableName);
|
|
136
|
+
for (const panel of view.recordViewPanels || []) {
|
|
137
|
+
if (panel.tableName) tables.add(panel.tableName);
|
|
138
|
+
if (panel.createConfig?.tableName) tables.add(panel.createConfig.tableName);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return tables;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Reject client-supplied SQL fragments (the `filters` array is concatenated
|
|
145
|
+
// raw into the WHERE clause). With multipleStatements off this already blocks
|
|
146
|
+
// stacked writes; the pattern also blocks UNION/comment/file exfil breakouts.
|
|
147
|
+
const looksLikeSqlBreakout = (frag) =>
|
|
148
|
+
typeof frag === "string" &&
|
|
149
|
+
/(;|--|\/\*|\bunion\b|\binto\s+(out|dump)file\b|\bsleep\s*\(|\bbenchmark\s*\(|\bload_file\s*\()/i.test(
|
|
150
|
+
frag
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
app.use((req, res, next) => {
|
|
154
|
+
const role = roleOf(req);
|
|
155
|
+
|
|
156
|
+
// 1) Validate any raw filter fragments on read endpoints.
|
|
157
|
+
if (req.body && Array.isArray(req.body.filters)) {
|
|
158
|
+
if (req.body.filters.some(looksLikeSqlBreakout)) {
|
|
159
|
+
return res.status(400).send({ error: "Invalid filter" });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 2) Enforce write authorization on mutating routes.
|
|
164
|
+
if (MUTATING_VIEW_ROUTES.has(req.path)) {
|
|
165
|
+
const view = viewFromRequest(req);
|
|
166
|
+
if (!view || !canWriteView(view, role)) {
|
|
167
|
+
return res.sendStatus(403);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
next();
|
|
172
|
+
});
|
|
173
|
+
|
|
79
174
|
async function getRecords(params, computeCount = false) {
|
|
80
175
|
let queryString = "";
|
|
81
176
|
if (params.filters) {
|
|
@@ -709,6 +804,9 @@ export default (db, config) => {
|
|
|
709
804
|
|
|
710
805
|
app.post("/updateRecord", async function (req, res, next) {
|
|
711
806
|
const view = req.config.views[req.body.viewIndex];
|
|
807
|
+
if (!areSafeColumnNames(req.body.data)) {
|
|
808
|
+
return res.status(400).send({ error: "Invalid column name" });
|
|
809
|
+
}
|
|
712
810
|
for (let key in req.body.data) {
|
|
713
811
|
if (
|
|
714
812
|
req.body.data[key] == "NULL" ||
|
|
@@ -921,6 +1019,15 @@ export default (db, config) => {
|
|
|
921
1019
|
});
|
|
922
1020
|
|
|
923
1021
|
app.post("/duplicateRecord", async function (req, res, next) {
|
|
1022
|
+
// SECURITY: bound the (client-supplied) target table to tables the role may write.
|
|
1023
|
+
const allowedTables = writableTables(req);
|
|
1024
|
+
const requestedTables = [
|
|
1025
|
+
req.body.table,
|
|
1026
|
+
...(req.body.duplicateConfig?.foreignTables || []).map((t) => t.table),
|
|
1027
|
+
];
|
|
1028
|
+
if (requestedTables.some((t) => !allowedTables.has(t))) {
|
|
1029
|
+
return res.sendStatus(403);
|
|
1030
|
+
}
|
|
924
1031
|
const primaryId = req.body.duplicateConfig.primaryId;
|
|
925
1032
|
const { err, rows } = await db.query("SELECT * FROM ?? WHERE ?? = ?", [
|
|
926
1033
|
req.body.table,
|
|
@@ -998,6 +1105,9 @@ export default (db, config) => {
|
|
|
998
1105
|
delete createPayload[key];
|
|
999
1106
|
}
|
|
1000
1107
|
}
|
|
1108
|
+
if (!areSafeColumnNames(createPayload)) {
|
|
1109
|
+
return res.status(400).send({ error: "Invalid column name" });
|
|
1110
|
+
}
|
|
1001
1111
|
const queryExpression = Object.keys(createPayload).join(", ");
|
|
1002
1112
|
const queryPlaceholders = Object.keys(createPayload)
|
|
1003
1113
|
.map((x) => "?")
|
|
@@ -1047,30 +1157,69 @@ export default (db, config) => {
|
|
|
1047
1157
|
}
|
|
1048
1158
|
});
|
|
1049
1159
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
if (
|
|
1057
|
-
|
|
1160
|
+
// Collect every SQL template declared anywhere in the (role-filtered) config:
|
|
1161
|
+
// panel `query`, `autocompleteListQuery`, etc. customQuery may only run a
|
|
1162
|
+
// template that already exists in the config the caller is allowed to see —
|
|
1163
|
+
// so it can never be turned into an arbitrary read/write primitive.
|
|
1164
|
+
function collectAllowedQueries(node, acc = new Set<string>()) {
|
|
1165
|
+
if (!node || typeof node !== "object") return acc;
|
|
1166
|
+
if (Array.isArray(node)) {
|
|
1167
|
+
for (const item of node) collectAllowedQueries(item, acc);
|
|
1168
|
+
return acc;
|
|
1058
1169
|
}
|
|
1059
|
-
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1170
|
+
for (const key of Object.keys(node)) {
|
|
1171
|
+
const value = node[key];
|
|
1172
|
+
if (typeof value === "string" && (key === "query" || /Query$/.test(key))) {
|
|
1173
|
+
acc.add(value.trim());
|
|
1174
|
+
} else if (value && typeof value === "object") {
|
|
1175
|
+
collectAllowedQueries(value, acc);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return acc;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Attack-surface reduction: only expose /customQuery at all when the config
|
|
1182
|
+
// actually declares SQL templates (panels, autocomplete lists, …). If nothing
|
|
1183
|
+
// uses it, the route is replaced by an explicit "disabled" stub so the raw-SQL
|
|
1184
|
+
// sink simply does not exist for that deployment.
|
|
1185
|
+
const CONFIG_USES_CUSTOM_QUERY = collectAllowedQueries(config.views).size > 0;
|
|
1186
|
+
|
|
1187
|
+
if (!CONFIG_USES_CUSTOM_QUERY) {
|
|
1188
|
+
app.get("/customQuery", (req, res) => res.sendStatus(404));
|
|
1189
|
+
} else {
|
|
1190
|
+
app.get("/customQuery", async function (req, res, next) {
|
|
1191
|
+
const template = typeof req.query.query === "string" ? req.query.query : "";
|
|
1192
|
+
// SECURITY: only run whitelisted, read-only templates, and bind the record id
|
|
1193
|
+
// as a parameter instead of substituting it into the SQL string.
|
|
1194
|
+
const allowed = collectAllowedQueries(req.config.views);
|
|
1195
|
+
if (!allowed.has(template.trim())) {
|
|
1196
|
+
return res.status(403).send({ error: "Query not allowed" });
|
|
1197
|
+
}
|
|
1198
|
+
if (!/^\s*\(?\s*(select|with)\b/i.test(template) || /;/.test(template)) {
|
|
1199
|
+
return res.status(400).send({ error: "Only read-only queries are allowed" });
|
|
1200
|
+
}
|
|
1201
|
+
const placeholderCount = (template.match(/\?/g) || []).length;
|
|
1202
|
+
const params = new Array(placeholderCount).fill(req.query.primaryValue);
|
|
1203
|
+
const { err, rows } = await db.query(template, params);
|
|
1204
|
+
if (err) {
|
|
1205
|
+
return res.status(500).send(err);
|
|
1206
|
+
}
|
|
1207
|
+
if (req.query.viewIndex && req.query.panelIndex) {
|
|
1208
|
+
const panel =
|
|
1209
|
+
req.config.views[req.query.viewIndex as string].recordViewPanels[
|
|
1210
|
+
req.query.panelIndex as string
|
|
1211
|
+
];
|
|
1212
|
+
if (panel.formatValue) {
|
|
1213
|
+
for (const row of rows) {
|
|
1214
|
+
for (const key in panel.formatValue) {
|
|
1215
|
+
row[key] = await panel.formatValue[key](row);
|
|
1216
|
+
}
|
|
1068
1217
|
}
|
|
1069
1218
|
}
|
|
1070
1219
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
}
|
|
1220
|
+
res.send(rows);
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1074
1223
|
|
|
1075
1224
|
function uploadFile(column, fileParamName, req, res) {
|
|
1076
1225
|
if (column.s3Config || column.gcpConfig) {
|
|
@@ -1132,28 +1281,42 @@ export default (db, config) => {
|
|
|
1132
1281
|
Math.random().toString().substr(2) +
|
|
1133
1282
|
"." +
|
|
1134
1283
|
extension;
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1284
|
+
const rawHost = String(s3Config.host || "");
|
|
1285
|
+
const endpoint = /^https?:\/\//i.test(rawHost)
|
|
1286
|
+
? rawHost
|
|
1287
|
+
: `https://${rawHost}`;
|
|
1288
|
+
const s3 = new S3Client({
|
|
1289
|
+
endpoint,
|
|
1139
1290
|
region: s3Config.region,
|
|
1140
|
-
|
|
1291
|
+
credentials: {
|
|
1292
|
+
accessKeyId: s3Config.accessKeyId,
|
|
1293
|
+
secretAccessKey: s3Config.secretAccessKey,
|
|
1294
|
+
},
|
|
1295
|
+
forcePathStyle: false,
|
|
1141
1296
|
});
|
|
1142
1297
|
if (s3Config.signedUrlNeeded) {
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1298
|
+
getSignedUrl(
|
|
1299
|
+
s3,
|
|
1300
|
+
new PutObjectCommand({
|
|
1301
|
+
Bucket: s3Config.bucket,
|
|
1302
|
+
Key: path,
|
|
1303
|
+
ContentType: contentType,
|
|
1304
|
+
}),
|
|
1305
|
+
{ expiresIn: 150 }
|
|
1306
|
+
)
|
|
1307
|
+
.then((signedUrl) =>
|
|
1308
|
+
axios.put(
|
|
1309
|
+
signedUrl,
|
|
1310
|
+
fs.readFileSync(files[fileParamName][0].path),
|
|
1311
|
+
{
|
|
1312
|
+
maxContentLength: 100000000000,
|
|
1313
|
+
maxBodyLength: 100000000000,
|
|
1314
|
+
headers: {
|
|
1315
|
+
"Content-Type": contentType,
|
|
1316
|
+
},
|
|
1317
|
+
}
|
|
1318
|
+
)
|
|
1319
|
+
)
|
|
1157
1320
|
.then(() => {
|
|
1158
1321
|
res.status(200).send({
|
|
1159
1322
|
path: s3Config.pathTransform
|
|
@@ -1167,16 +1330,16 @@ export default (db, config) => {
|
|
|
1167
1330
|
res.status(500).send(e);
|
|
1168
1331
|
});
|
|
1169
1332
|
} else {
|
|
1170
|
-
s3.
|
|
1171
|
-
{
|
|
1333
|
+
s3.send(
|
|
1334
|
+
new PutObjectCommand({
|
|
1172
1335
|
Body: fs.readFileSync(files[fileParamName][0].path),
|
|
1173
1336
|
Bucket: s3Config.bucket,
|
|
1174
1337
|
ContentType: contentType,
|
|
1175
1338
|
ACL: "public-read",
|
|
1176
1339
|
Key: path,
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1340
|
+
})
|
|
1341
|
+
)
|
|
1342
|
+
.then(() => {
|
|
1180
1343
|
res.send({
|
|
1181
1344
|
url: `https://${s3Config.host}/${s3Config.bucket}/${path}`,
|
|
1182
1345
|
path: s3Config.pathTransform
|
|
@@ -1185,8 +1348,8 @@ export default (db, config) => {
|
|
|
1185
1348
|
aspectRatio,
|
|
1186
1349
|
originalFilename,
|
|
1187
1350
|
});
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1351
|
+
})
|
|
1352
|
+
.catch((err) => res.status(500).send(err));
|
|
1190
1353
|
}
|
|
1191
1354
|
}
|
|
1192
1355
|
});
|
package/src/index.ts
CHANGED
|
@@ -130,7 +130,11 @@ export default (config: Config): any => {
|
|
|
130
130
|
// we check that the user still exists
|
|
131
131
|
let stillExists = false;
|
|
132
132
|
for (const authRole of config.authRoles) {
|
|
133
|
-
|
|
133
|
+
// Re-apply the role's `filters` here too, so an account that becomes
|
|
134
|
+
// ineligible (e.g. deactivated) has its existing session invalidated
|
|
135
|
+
// on its next request — not just blocked at login time.
|
|
136
|
+
const filters = authRole.filters ? `AND ${authRole.filters}` : "";
|
|
137
|
+
const { rows } = await db.query(`SELECT * FROM ?? WHERE ?? = ? ${filters}`, [authRole.table, authRole.idField, req.session.user.userId]);
|
|
134
138
|
if (rows.length > 0) {
|
|
135
139
|
stillExists = true;
|
|
136
140
|
break;
|
package/src/utils/dbManager.ts
CHANGED
|
@@ -89,7 +89,10 @@ export default class dbManager {
|
|
|
89
89
|
port: config.mysql.port || 3306,
|
|
90
90
|
charset: 'UTF8MB4_GENERAL_CI',
|
|
91
91
|
dateStrings: true,
|
|
92
|
-
|
|
92
|
+
// SECURITY: multi-statement queries let any SQL-injection escalate to stacked
|
|
93
|
+
// writes / DROP. Keep OFF by default; opt in explicitly only if a config truly
|
|
94
|
+
// relies on `;`-separated statements (none of galadmin's internal queries do).
|
|
95
|
+
multipleStatements: config.mysql.multipleStatements === true
|
|
93
96
|
});
|
|
94
97
|
this.dbType = "MYSQL";
|
|
95
98
|
} else if (config.postgresql) {
|