kryptheon-night 0.1.5 → 0.1.7
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 +21 -0
- package/bin/kryptheon-night.js +61 -2
- package/engine.sql +2540 -2484
- package/finding.js +16 -0
- package/installer.js +15 -0
- package/outside.js +409 -0
- package/package.json +6 -3
- package/schema.js +1137 -1058
- package/trouble.js +19 -0
package/README.md
CHANGED
|
@@ -37,6 +37,27 @@ If you paste the wrong thing — the project URL, an API key, the line with
|
|
|
37
37
|
`[YOUR-PASSWORD]` still in it — it says so and tells you where the right one
|
|
38
38
|
is. It does not just fail.
|
|
39
39
|
|
|
40
|
+
## No connection string? Check your app from outside
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
npx kryptheon-night https://your-app.example.com
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Give it your running app instead. It reads the app the way a stranger's
|
|
47
|
+
browser does — the Supabase address and public key every visitor is already
|
|
48
|
+
handed — and asks each table your app uses one question: can a stranger read
|
|
49
|
+
this? It asks for a **count only**, so no customer's row is ever pulled out,
|
|
50
|
+
and it writes nothing. About ten seconds, no password, no yes to give.
|
|
51
|
+
|
|
52
|
+
It is a narrower check than the full scan, and it says so:
|
|
53
|
+
|
|
54
|
+
- It only proves a leak when a stranger actually gets rows back. A table that
|
|
55
|
+
answers with none looks the same whether it is well protected or open and
|
|
56
|
+
empty, so it is reported as "could not tell", never as safe.
|
|
57
|
+
- One customer reading another's rows, adding or deleting rows, and the same
|
|
58
|
+
thing saved twice at once all change data, so they are only ever tried on
|
|
59
|
+
a copy — the full scan above.
|
|
60
|
+
|
|
40
61
|
## What you get back
|
|
41
62
|
|
|
42
63
|
Four questions, asked against the copy:
|
package/bin/kryptheon-night.js
CHANGED
|
@@ -32,6 +32,7 @@ const trouble = require('../trouble.js');
|
|
|
32
32
|
const { howToConnect } = require('../connect.js');
|
|
33
33
|
const installer = require('../installer.js');
|
|
34
34
|
const finding = require('../finding.js');
|
|
35
|
+
const outside = require('../outside.js');
|
|
35
36
|
|
|
36
37
|
const line = (text) => process.stdout.write(text + '\n');
|
|
37
38
|
const fail = (text) => process.stderr.write(text + '\n');
|
|
@@ -45,6 +46,12 @@ function usage() {
|
|
|
45
46
|
'',
|
|
46
47
|
' npx kryptheon-night',
|
|
47
48
|
'',
|
|
49
|
+
' No connection string? Give it your running app instead. It reads the app',
|
|
50
|
+
' the way a stranger does and tells you which tables anyone can read -',
|
|
51
|
+
' counts only, nothing written:',
|
|
52
|
+
'',
|
|
53
|
+
' npx kryptheon-night https://your-app.example.com',
|
|
54
|
+
'',
|
|
48
55
|
' Run it again after you fix something and it tells you what is actually',
|
|
49
56
|
' closed - you do not have to ask it to. It also offers to do the same',
|
|
50
57
|
' check every night from inside your database, and says so if it already',
|
|
@@ -85,11 +92,15 @@ const VERBS = ['install', 'uninstall', 'status', 'night'];
|
|
|
85
92
|
/** What was asked for, and what was left to the default. */
|
|
86
93
|
function readArgs(argv) {
|
|
87
94
|
const asked = {
|
|
88
|
-
command: 'scan', schema: null, recheck: false, yes: false, help: false, unknown: [],
|
|
95
|
+
command: 'scan', schema: null, appUrl: null, recheck: false, yes: false, help: false, unknown: [],
|
|
89
96
|
};
|
|
90
97
|
for (let i = 0; i < argv.length; i++) {
|
|
91
98
|
const arg = argv[i];
|
|
92
|
-
|
|
99
|
+
// An address is the outside attack: the running app, read the way a
|
|
100
|
+
// stranger's browser reads it. Never a schema name - no schema is called
|
|
101
|
+
// https://anything.
|
|
102
|
+
if (/^https?:\/\//i.test(arg) && !asked.appUrl) asked.appUrl = arg;
|
|
103
|
+
else if (arg === '--recheck') asked.recheck = true;
|
|
93
104
|
else if (arg === '--yes' || arg === '-y') asked.yes = true;
|
|
94
105
|
else if (arg === '--help' || arg === '-h') asked.help = true;
|
|
95
106
|
else if (arg === '--schema') asked.schema = argv[++i] || null;
|
|
@@ -322,6 +333,45 @@ async function offerTheNight(client, target, canAsk) {
|
|
|
322
333
|
}
|
|
323
334
|
}
|
|
324
335
|
|
|
336
|
+
/**
|
|
337
|
+
* The attack from outside, end to end: find the backend in the app, ask each
|
|
338
|
+
* table as a stranger, say what came back and what could not be tested.
|
|
339
|
+
*
|
|
340
|
+
* Exit codes are the same three as the scan: 1 when a stranger read rows, 0
|
|
341
|
+
* when nothing came back, 2 when it could not run at all.
|
|
342
|
+
*/
|
|
343
|
+
async function runOutside(appUrl) {
|
|
344
|
+
line('');
|
|
345
|
+
line(' Kryptheon Night Shift - from outside');
|
|
346
|
+
line(' I read your app the way a stranger\'s browser does, and ask your');
|
|
347
|
+
line(' database what it lets a stranger see. Counts only, nothing written.');
|
|
348
|
+
line('');
|
|
349
|
+
line(' Reading ' + appUrl + ' ...');
|
|
350
|
+
|
|
351
|
+
let found;
|
|
352
|
+
try {
|
|
353
|
+
found = await outside.discover(appUrl);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
fail('');
|
|
356
|
+
fail(' ' + (err.code ? err.message : 'I could not read ' + appUrl + ': ' + err.message));
|
|
357
|
+
fail('');
|
|
358
|
+
return 2;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let result;
|
|
362
|
+
try {
|
|
363
|
+
result = await outside.prowl(found);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
fail('');
|
|
366
|
+
fail(' I found your database but could not ask it anything: ' + err.message);
|
|
367
|
+
fail('');
|
|
368
|
+
return 2;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
outside.reportLines(found, result).forEach(line);
|
|
372
|
+
return result.findings.length ? 1 : 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
325
375
|
/** Prints a block of plain lines with the indent the rest of the report uses. */
|
|
326
376
|
function block(lines, write) {
|
|
327
377
|
(write || line)('');
|
|
@@ -343,6 +393,15 @@ async function main() {
|
|
|
343
393
|
process.exit(2);
|
|
344
394
|
}
|
|
345
395
|
|
|
396
|
+
// The outside attack. Needs no connection string, no copy and no yes: it
|
|
397
|
+
// only asks the live app what it already answers any visitor, and asks for
|
|
398
|
+
// counts, never rows. So nothing below - the string, the consent screen,
|
|
399
|
+
// the connection - applies to it.
|
|
400
|
+
if (asked.appUrl) {
|
|
401
|
+
process.exitCode = await runOutside(asked.appUrl);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
346
405
|
line('');
|
|
347
406
|
line(' Kryptheon Night Shift');
|
|
348
407
|
line(' I attack a copy of your database and tell you what got in.');
|