@galadrim/galadmin 0.1.230 → 0.1.232

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.
@@ -74,7 +74,7 @@ class dbManager {
74
74
  port: config.mysql.port || 3306,
75
75
  charset: 'UTF8MB4_GENERAL_CI',
76
76
  dateStrings: true,
77
- multipleStatements: true
77
+ multipleStatements: config.mysql.multipleStatements === true
78
78
  });
79
79
  this.dbType = "MYSQL";
80
80
  }
@@ -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.230",
3
+ "version": "0.1.232",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
package/src/api.ts CHANGED
@@ -76,6 +76,100 @@ export default (db, config) => {
76
76
  next();
77
77
  });
78
78
 
79
+ // ---------------------------------------------------------------------------
80
+ // SECURITY: server-side authorization.
81
+ //
82
+ // The config-filtering middleware above only decides which views the *client*
83
+ // is shown, and sets canEdit/canCreate/canDelete flags on that served config.
84
+ // Nothing enforced those decisions server-side, so a session with the lowest
85
+ // role could POST directly to /updateRecord, /deleteRecord, /customQuery, …
86
+ // and act on any view — including read-only ones — as the DB user (root).
87
+ // This block re-checks every sensitive route against the view's declared roles.
88
+ // ---------------------------------------------------------------------------
89
+ const ROOT_ROLE = "root";
90
+ const roleOf = (req) => req.session && req.session.user && req.session.user.role;
91
+ // Write access = the view's `roles` (root always allowed).
92
+ const canWriteView = (view, role) =>
93
+ role === ROOT_ROLE || !view || !view.roles || view.roles.includes(role);
94
+
95
+ const viewFromRequest = (req) => {
96
+ const idx =
97
+ req.body && req.body.viewIndex !== undefined
98
+ ? req.body.viewIndex
99
+ : req.query && req.query.viewIndex !== undefined
100
+ ? req.query.viewIndex
101
+ : undefined;
102
+ if (idx === undefined || !req.config || !req.config.views) return undefined;
103
+ return req.config.views[idx];
104
+ };
105
+
106
+ // Routes that mutate data and reference their target view via viewIndex
107
+ // (body for POST bodies, query for /uploadFile). Import routes carry the
108
+ // viewIndex inside a multipart body and are guarded inline in their handlers.
109
+ const MUTATING_VIEW_ROUTES = new Set([
110
+ "/updateRecord",
111
+ "/createRecord",
112
+ "/deleteRecord",
113
+ "/deleteRecords",
114
+ "/deleteRecordFromPanel",
115
+ "/customAction",
116
+ "/uploadFile",
117
+ ]);
118
+ // /duplicateRecord references its table via req.body.table (not a viewIndex),
119
+ // so it is guarded inline in its handler instead of by the generic middleware.
120
+
121
+ // Column names are interpolated into INSERT/UPDATE statements; only accept plain
122
+ // SQL identifiers so a crafted key cannot break out of the column list.
123
+ const areSafeColumnNames = (data) =>
124
+ !data ||
125
+ Object.keys(data).every((k) => /^[A-Za-z0-9_]+$/.test(k));
126
+
127
+ // Tables the current role is allowed to write, gathered from its writable views
128
+ // and their creatable panels — used to bound /duplicateRecord's target table.
129
+ const writableTables = (req) => {
130
+ const role = roleOf(req);
131
+ const tables = new Set<string>();
132
+ for (const view of req.config?.views || []) {
133
+ if (!canWriteView(view, role)) continue;
134
+ if (view.tableName) tables.add(view.tableName);
135
+ for (const panel of view.recordViewPanels || []) {
136
+ if (panel.tableName) tables.add(panel.tableName);
137
+ if (panel.createConfig?.tableName) tables.add(panel.createConfig.tableName);
138
+ }
139
+ }
140
+ return tables;
141
+ };
142
+
143
+ // Reject client-supplied SQL fragments (the `filters` array is concatenated
144
+ // raw into the WHERE clause). With multipleStatements off this already blocks
145
+ // stacked writes; the pattern also blocks UNION/comment/file exfil breakouts.
146
+ const looksLikeSqlBreakout = (frag) =>
147
+ typeof frag === "string" &&
148
+ /(;|--|\/\*|\bunion\b|\binto\s+(out|dump)file\b|\bsleep\s*\(|\bbenchmark\s*\(|\bload_file\s*\()/i.test(
149
+ frag
150
+ );
151
+
152
+ app.use((req, res, next) => {
153
+ const role = roleOf(req);
154
+
155
+ // 1) Validate any raw filter fragments on read endpoints.
156
+ if (req.body && Array.isArray(req.body.filters)) {
157
+ if (req.body.filters.some(looksLikeSqlBreakout)) {
158
+ return res.status(400).send({ error: "Invalid filter" });
159
+ }
160
+ }
161
+
162
+ // 2) Enforce write authorization on mutating routes.
163
+ if (MUTATING_VIEW_ROUTES.has(req.path)) {
164
+ const view = viewFromRequest(req);
165
+ if (!view || !canWriteView(view, role)) {
166
+ return res.sendStatus(403);
167
+ }
168
+ }
169
+
170
+ next();
171
+ });
172
+
79
173
  async function getRecords(params, computeCount = false) {
80
174
  let queryString = "";
81
175
  if (params.filters) {
@@ -709,6 +803,9 @@ export default (db, config) => {
709
803
 
710
804
  app.post("/updateRecord", async function (req, res, next) {
711
805
  const view = req.config.views[req.body.viewIndex];
806
+ if (!areSafeColumnNames(req.body.data)) {
807
+ return res.status(400).send({ error: "Invalid column name" });
808
+ }
712
809
  for (let key in req.body.data) {
713
810
  if (
714
811
  req.body.data[key] == "NULL" ||
@@ -921,6 +1018,15 @@ export default (db, config) => {
921
1018
  });
922
1019
 
923
1020
  app.post("/duplicateRecord", async function (req, res, next) {
1021
+ // SECURITY: bound the (client-supplied) target table to tables the role may write.
1022
+ const allowedTables = writableTables(req);
1023
+ const requestedTables = [
1024
+ req.body.table,
1025
+ ...(req.body.duplicateConfig?.foreignTables || []).map((t) => t.table),
1026
+ ];
1027
+ if (requestedTables.some((t) => !allowedTables.has(t))) {
1028
+ return res.sendStatus(403);
1029
+ }
924
1030
  const primaryId = req.body.duplicateConfig.primaryId;
925
1031
  const { err, rows } = await db.query("SELECT * FROM ?? WHERE ?? = ?", [
926
1032
  req.body.table,
@@ -998,6 +1104,9 @@ export default (db, config) => {
998
1104
  delete createPayload[key];
999
1105
  }
1000
1106
  }
1107
+ if (!areSafeColumnNames(createPayload)) {
1108
+ return res.status(400).send({ error: "Invalid column name" });
1109
+ }
1001
1110
  const queryExpression = Object.keys(createPayload).join(", ");
1002
1111
  const queryPlaceholders = Object.keys(createPayload)
1003
1112
  .map((x) => "?")
@@ -1047,30 +1156,69 @@ export default (db, config) => {
1047
1156
  }
1048
1157
  });
1049
1158
 
1050
- app.get("/customQuery", async function (req, res, next) {
1051
- const query = (req.query.query as string).replace(
1052
- /\?/g,
1053
- req.query.primaryValue as string
1054
- );
1055
- const { err, rows } = await db.query(query);
1056
- if (err) {
1057
- return res.status(500).send(err);
1159
+ // Collect every SQL template declared anywhere in the (role-filtered) config:
1160
+ // panel `query`, `autocompleteListQuery`, etc. customQuery may only run a
1161
+ // template that already exists in the config the caller is allowed to see —
1162
+ // so it can never be turned into an arbitrary read/write primitive.
1163
+ function collectAllowedQueries(node, acc = new Set<string>()) {
1164
+ if (!node || typeof node !== "object") return acc;
1165
+ if (Array.isArray(node)) {
1166
+ for (const item of node) collectAllowedQueries(item, acc);
1167
+ return acc;
1168
+ }
1169
+ for (const key of Object.keys(node)) {
1170
+ const value = node[key];
1171
+ if (typeof value === "string" && (key === "query" || /Query$/.test(key))) {
1172
+ acc.add(value.trim());
1173
+ } else if (value && typeof value === "object") {
1174
+ collectAllowedQueries(value, acc);
1175
+ }
1058
1176
  }
1059
- if (req.query.viewIndex && req.query.panelIndex) {
1060
- const panel =
1061
- req.config.views[req.query.viewIndex as string].recordViewPanels[
1062
- req.query.panelIndex as string
1063
- ];
1064
- if (panel.formatValue) {
1065
- for (const row of rows) {
1066
- for (const key in panel.formatValue) {
1067
- row[key] = await panel.formatValue[key](row);
1177
+ return acc;
1178
+ }
1179
+
1180
+ // Attack-surface reduction: only expose /customQuery at all when the config
1181
+ // actually declares SQL templates (panels, autocomplete lists, …). If nothing
1182
+ // uses it, the route is replaced by an explicit "disabled" stub so the raw-SQL
1183
+ // sink simply does not exist for that deployment.
1184
+ const CONFIG_USES_CUSTOM_QUERY = collectAllowedQueries(config.views).size > 0;
1185
+
1186
+ if (!CONFIG_USES_CUSTOM_QUERY) {
1187
+ app.get("/customQuery", (req, res) => res.sendStatus(404));
1188
+ } else {
1189
+ app.get("/customQuery", async function (req, res, next) {
1190
+ const template = typeof req.query.query === "string" ? req.query.query : "";
1191
+ // SECURITY: only run whitelisted, read-only templates, and bind the record id
1192
+ // as a parameter instead of substituting it into the SQL string.
1193
+ const allowed = collectAllowedQueries(req.config.views);
1194
+ if (!allowed.has(template.trim())) {
1195
+ return res.status(403).send({ error: "Query not allowed" });
1196
+ }
1197
+ if (!/^\s*\(?\s*(select|with)\b/i.test(template) || /;/.test(template)) {
1198
+ return res.status(400).send({ error: "Only read-only queries are allowed" });
1199
+ }
1200
+ const placeholderCount = (template.match(/\?/g) || []).length;
1201
+ const params = new Array(placeholderCount).fill(req.query.primaryValue);
1202
+ const { err, rows } = await db.query(template, params);
1203
+ if (err) {
1204
+ return res.status(500).send(err);
1205
+ }
1206
+ if (req.query.viewIndex && req.query.panelIndex) {
1207
+ const panel =
1208
+ req.config.views[req.query.viewIndex as string].recordViewPanels[
1209
+ req.query.panelIndex as string
1210
+ ];
1211
+ if (panel.formatValue) {
1212
+ for (const row of rows) {
1213
+ for (const key in panel.formatValue) {
1214
+ row[key] = await panel.formatValue[key](row);
1215
+ }
1068
1216
  }
1069
1217
  }
1070
1218
  }
1071
- }
1072
- res.send(rows);
1073
- });
1219
+ res.send(rows);
1220
+ });
1221
+ }
1074
1222
 
1075
1223
  function uploadFile(column, fileParamName, req, res) {
1076
1224
  if (column.s3Config || column.gcpConfig) {
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
- const { rows } = await db.query("SELECT * FROM ?? WHERE ?? = ?", [authRole.table, authRole.idField, req.session.user.userId]);
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;
@@ -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
- multipleStatements: true
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) {
@@ -1,12 +0,0 @@
1
- #!/bin/sh
2
- basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
-
4
- case `uname` in
5
- *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
6
- esac
7
-
8
- if [ -x "$basedir/node" ]; then
9
- exec "$basedir/node" "$basedir/../semver/bin/semver.js" "$@"
10
- else
11
- exec node "$basedir/../semver/bin/semver.js" "$@"
12
- fi
@@ -1,17 +0,0 @@
1
- @ECHO off
2
- GOTO start
3
- :find_dp0
4
- SET dp0=%~dp0
5
- EXIT /b
6
- :start
7
- SETLOCAL
8
- CALL :find_dp0
9
-
10
- IF EXIST "%dp0%\node.exe" (
11
- SET "_prog=%dp0%\node.exe"
12
- ) ELSE (
13
- SET "_prog=node"
14
- SET PATHEXT=%PATHEXT:;.JS;=;%
15
- )
16
-
17
- endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\semver\bin\semver.js" %*
@@ -1,28 +0,0 @@
1
- #!/usr/bin/env pwsh
2
- $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
3
-
4
- $exe=""
5
- if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
6
- # Fix case when both the Windows and Linux builds of Node
7
- # are installed in the same directory
8
- $exe=".exe"
9
- }
10
- $ret=0
11
- if (Test-Path "$basedir/node$exe") {
12
- # Support pipeline input
13
- if ($MyInvocation.ExpectingInput) {
14
- $input | & "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args
15
- } else {
16
- & "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args
17
- }
18
- $ret=$LASTEXITCODE
19
- } else {
20
- # Support pipeline input
21
- if ($MyInvocation.ExpectingInput) {
22
- $input | & "node$exe" "$basedir/../semver/bin/semver.js" $args
23
- } else {
24
- & "node$exe" "$basedir/../semver/bin/semver.js" $args
25
- }
26
- $ret=$LASTEXITCODE
27
- }
28
- exit $ret