kryptheon-night 0.1.25 → 0.1.27
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/README.md +11 -0
- package/attack.js +63 -2
- package/bin/kryptheon-night.js +3 -1
- package/engine.sql +571 -3
- package/finding.js +163 -3
- package/intro.js +5 -1
- package/package.json +5 -3
- package/recheck.js +138 -13
- package/scan.js +75 -6
- package/schema.js +59 -0
- package/sqlengine.js +12 -1
- package/tamper.js +334 -1
package/README.md
CHANGED
|
@@ -69,6 +69,17 @@ Four questions, asked against the copy:
|
|
|
69
69
|
| **Collision** | can the same thing exist twice? |
|
|
70
70
|
| **Interruption** | can a half-finished write survive? |
|
|
71
71
|
|
|
72
|
+
And three things it says are **yours to check**, because only you know
|
|
73
|
+
whether they are meant:
|
|
74
|
+
|
|
75
|
+
| check | the question |
|
|
76
|
+
|---|---|
|
|
77
|
+
| **Lowest role** | can a viewer in a team change or add to what is in it, and which roles can read its tokens, keys and payloads? (tried on the copy, undone) |
|
|
78
|
+
| **Storage** | is a bucket public although a rule says who may read it? (settings only, never a file) |
|
|
79
|
+
| **Open functions** | can a visitor with no account call a function that skips the rules? |
|
|
80
|
+
|
|
81
|
+
These are listed apart from problems and do not change the exit code.
|
|
82
|
+
|
|
72
83
|
A problem reads like this:
|
|
73
84
|
|
|
74
85
|
CRITICAL customers
|
package/attack.js
CHANGED
|
@@ -204,6 +204,48 @@ function allowedByCheck(table, columnName) {
|
|
|
204
204
|
return found;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Sets of columns where exactly one may be filled in.
|
|
209
|
+
*
|
|
210
|
+
* Found on a blind test (HelixOps): documents belonged to a work order or to
|
|
211
|
+
* an inspection, never both, written
|
|
212
|
+
* CHECK ((work_order_id IS NOT NULL)::int + (inspection_id IS NOT NULL)::int = 1)
|
|
213
|
+
* Both parents were there, so the seeder pointed the row at both, the CHECK
|
|
214
|
+
* refused every attempt, and the table was never tested at all.
|
|
215
|
+
*
|
|
216
|
+
* Two spellings are read - that sum, and num_nonnulls(a, b, ...) = 1 - and
|
|
217
|
+
* only when the constraint says nothing else. A CHECK with anything more in it
|
|
218
|
+
* is left alone rather than half understood.
|
|
219
|
+
*/
|
|
220
|
+
function exactlyOne(table) {
|
|
221
|
+
const groups = [];
|
|
222
|
+
const name = '("(?:[^"]|"")+"|[A-Za-z_][A-Za-z0-9_$]*)';
|
|
223
|
+
const unquote = (text) => (text.charAt(0) === '"' ? text.slice(1, -1).split('""').join('"') : text);
|
|
224
|
+
for (const constraint of table.constraints || []) {
|
|
225
|
+
if (constraint.kind !== 'c') continue;
|
|
226
|
+
const definition = String(constraint.definition || '');
|
|
227
|
+
let columns = [];
|
|
228
|
+
let rest = definition;
|
|
229
|
+
const summed = new RegExp('\\(\\(' + name + ' IS NOT NULL\\)\\)::integer', 'gi');
|
|
230
|
+
let match;
|
|
231
|
+
while ((match = summed.exec(definition)) !== null) columns.push(unquote(match[1]));
|
|
232
|
+
rest = rest.replace(summed, '').replace(/\+/g, '');
|
|
233
|
+
if (!columns.length) {
|
|
234
|
+
const counted = /num_nonnulls\(([^()]*)\)/i.exec(definition);
|
|
235
|
+
if (counted) {
|
|
236
|
+
const listed = new RegExp(name, 'g');
|
|
237
|
+
let one;
|
|
238
|
+
while ((one = listed.exec(counted[1])) !== null) columns.push(unquote(one[1]));
|
|
239
|
+
rest = rest.replace(counted[0], '');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Nothing may be left but the CHECK itself, brackets and "= 1".
|
|
243
|
+
if (columns.length < 2 || rest.replace(/[()\s]/g, '').toUpperCase() !== 'CHECK=1') continue;
|
|
244
|
+
groups.push(columns);
|
|
245
|
+
}
|
|
246
|
+
return groups;
|
|
247
|
+
}
|
|
248
|
+
|
|
207
249
|
/**
|
|
208
250
|
* The foreign keys on a table, read out of Postgres's own wording.
|
|
209
251
|
*
|
|
@@ -395,12 +437,28 @@ async function rowFor(client, schema, table, person, distinct, overrides, attemp
|
|
|
395
437
|
const partOfKey = new Set();
|
|
396
438
|
for (const key of foreignKeys(table)) key.columns.forEach((name) => partOfKey.add(name));
|
|
397
439
|
|
|
440
|
+
// Where exactly one of a set may be filled in: the one already chosen, or
|
|
441
|
+
// else the first that can be - a key needs a parent row to point at. The
|
|
442
|
+
// rest are written as nothing, whatever their default.
|
|
443
|
+
const chosen = (name) => Object.prototype.hasOwnProperty.call(forced, name);
|
|
444
|
+
const onlyOne = new Map();
|
|
445
|
+
for (const group of exactlyOne(table)) {
|
|
446
|
+
const kept = group.find(chosen) || group.find((name) => !partOfKey.has(name) || borrowed.has(name));
|
|
447
|
+
for (const name of group) onlyOne.set(name, name === kept);
|
|
448
|
+
}
|
|
449
|
+
|
|
398
450
|
for (const column of table.columns) {
|
|
399
451
|
if (Object.prototype.hasOwnProperty.call(forced, column.name)) {
|
|
400
452
|
columns.push(column.name);
|
|
401
453
|
values.push(forced[column.name]);
|
|
402
454
|
continue;
|
|
403
455
|
}
|
|
456
|
+
if (onlyOne.get(column.name) === false) {
|
|
457
|
+
columns.push(column.name);
|
|
458
|
+
values.push(null);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const mustFill = onlyOne.get(column.name) === true;
|
|
404
462
|
if (column.name === owner) {
|
|
405
463
|
columns.push(column.name);
|
|
406
464
|
values.push(person);
|
|
@@ -436,9 +494,11 @@ async function rowFor(client, schema, table, person, distinct, overrides, attemp
|
|
|
436
494
|
values.push(borrowed.get(column.name));
|
|
437
495
|
continue;
|
|
438
496
|
}
|
|
439
|
-
// Anything with a default can supply its own value.
|
|
497
|
+
// Anything with a default can supply its own value. A column that may be
|
|
498
|
+
// empty is left empty - unless it is the one of a set that has to be
|
|
499
|
+
// filled in.
|
|
440
500
|
if (column.default_expr) continue;
|
|
441
|
-
if (!column.not_null) continue;
|
|
501
|
+
if (!column.not_null && !mustFill) continue;
|
|
442
502
|
columns.push(column.name);
|
|
443
503
|
// A CHECK that lists what it will accept beats anything invented here.
|
|
444
504
|
const allowed = allowedByCheck(table, column.name);
|
|
@@ -766,6 +826,7 @@ module.exports = {
|
|
|
766
826
|
dependencyOrder: dependencyOrder,
|
|
767
827
|
valueFor: valueFor,
|
|
768
828
|
allowedByCheck: allowedByCheck,
|
|
829
|
+
exactlyOne: exactlyOne,
|
|
769
830
|
rowFor: rowFor,
|
|
770
831
|
existingRow: existingRow,
|
|
771
832
|
insertRow: insertRow,
|
package/bin/kryptheon-night.js
CHANGED
|
@@ -648,7 +648,9 @@ async function main() {
|
|
|
648
648
|
if (result.findings.length) scanner.report(result);
|
|
649
649
|
else scanner.notTestedLines(result).forEach(line);
|
|
650
650
|
scanner.saveRun(file, result);
|
|
651
|
-
|
|
651
|
+
// Two different databases: the verdict is a comparison, so the exit
|
|
652
|
+
// code is this run's own answer, the same as a first run.
|
|
653
|
+
process.exitCode = result.stopped ? 2 : verdict.elsewhere ? scanner.exitCodeFor(result) : verdict.allClear ? 0 : 1;
|
|
652
654
|
}
|
|
653
655
|
|
|
654
656
|
// And then the only other thing worth doing, offered rather than
|