fix(api-keys): gültige Schlüssel nie durch die Fehlversuchs-Sperre abweisen
Befunde aus dem Sicherheitsreview des Branches: - MEDIUM: Die Sperre lief vor der Schlüsselsuche und zählte auch falsch aufgebaute Tokens. Wer die gemeinsame Adresse (Cluster-Ausgang, Proxy) nutzt oder per X-Forwarded-For vortäuscht, hätte ERPNext und den Shop 15 Minuten aussperren können. Jetzt: erst Suche über den Hash; ein gültiger, nicht widerrufener, nicht abgelaufener Schlüssel kommt immer durch; 429 nur für unbekannte, widerrufene und abgelaufene Schlüssel; falsch aufgebaute Tokens sofort 401 ohne Zählung. Beim Start warnt ApiCore, wenn TRUST_PROXY in Produktion fehlt (im Cluster gesetzt). - LOW: Bei vollem Speicher verdrängt der Limiter zuerst nicht gesperrte Einträge, gesperrte erst zuletzt. - LOW: Bewegungs-Cursor mit Jahr außerhalb 2000-2100 ergab 500, jetzt 400. - LOW/INFO: Vertrag legt fest, dass ERPNext Produkte und Chargen bei jedem Lauf vollständig neu liest (Voll-Spiegel) und Bewegungen inkrementell; remainingQuantity ist live, Bewegungen kommen 5 Minuten verzögert. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
e28cc8e0f3
commit
fe7b61c3c1
11 files changed
+300
-81
No files matched your search
@@ -64,7 +64,30 @@ describe('ERP-Cursor', () => {
|
||||
'bewegungen',
|
||||
raw({ k: 'bewegungen', ts: '2026-10-01', id: 'c1' }),
|
||||
],
|
||||
[
|
||||
'Jahr jenseits des Datenbankbereichs',
|
||||
'bewegungen',
|
||||
raw({ k: 'bewegungen', ts: '+275760-09-13T00:00:00.000Z', id: 'c1' }),
|
||||
],
|
||||
[
|
||||
'Jahr vor 2000',
|
||||
'bewegungen',
|
||||
raw({ k: 'bewegungen', ts: '1999-12-31T23:59:59.999Z', id: 'c1' }),
|
||||
],
|
||||
[
|
||||
'Jahr nach 2100',
|
||||
'bewegungen',
|
||||
raw({ k: 'bewegungen', ts: '2101-01-01T00:00:00.000Z', id: 'c1' }),
|
||||
],
|
||||
] as const)('lehnt ab: %s', (_label, kind, cursor) => {
|
||||
expect(() => decodeCursor(cursor, kind)).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it.each(['2000-01-01T00:00:00.000Z', '2100-12-31T23:59:59.999Z'])(
|
||||
'nimmt den Rand des erlaubten Bereichs an: %s',
|
||||
(ts) => {
|
||||
const cursor = encodeCursor({ k: 'bewegungen', ts, id: 'c1' });
|
||||
expect(decodeCursor(cursor, 'bewegungen').ts).toBe(ts);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -23,6 +23,11 @@ type CursorOf<K extends ErpCursorKind> = Extract<ErpCursor, { k: K }>;
|
||||
// etwas anderes bedeuten könnten.
|
||||
const ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
||||
|
||||
// Zeitstempel eines Bewegungs-Cursors: nur plausible Jahre. JavaScript
|
||||
// kennt Jahre bis +275760, die Datenbankabfrage nicht - das wäre ein 500.
|
||||
const MIN_CURSOR_YEAR = 2000;
|
||||
const MAX_CURSOR_YEAR = 2100;
|
||||
|
||||
const FIELDS: Record<ErpCursorKind, readonly string[]> = {
|
||||
produkte: ['id', 'k'],
|
||||
chargen: ['id', 'k', 't'],
|
||||
@@ -55,7 +60,11 @@ function parse(raw: string): Record<string, unknown> {
|
||||
function isCanonicalTimestamp(value: unknown): value is string {
|
||||
if (typeof value !== 'string') return false;
|
||||
const date = new Date(value);
|
||||
return !Number.isNaN(date.getTime()) && date.toISOString() === value;
|
||||
if (Number.isNaN(date.getTime()) || date.toISOString() !== value) {
|
||||
return false;
|
||||
}
|
||||
const year = date.getUTCFullYear();
|
||||
return year >= MIN_CURSOR_YEAR && year <= MAX_CURSOR_YEAR;
|
||||
}
|
||||
|
||||
/** Prüft den Cursor für genau diesen Endpunkt; sonst 400. */
|
||||
|
||||
Reference in new issue
Block a user