Skip to content

Commit 532ba50

Browse files
committed
feat(bounded): Protect against hash collision attacks by introducing randomness
1 parent 666418c commit 532ba50

File tree

2 files changed

+42
-14
lines changed

2 files changed

+42
-14
lines changed

src/cache/bounded/index.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -429,11 +429,10 @@ export class BoundedCache<K extends KeyType, V> extends AbstractCache<K, V> impl
429429
*/
430430
while(data.weightedSize > data.weightedMaxSize) {
431431
const probation = data.probation.head.next;
432-
//const evicted = evictedToProbation == 0 ? null : data.probation.head.previous;
433-
const evicted = evictedToProbation === 0 ? data.probation.head : data.probation.head.previous;
432+
const evictedCandidate = evictedToProbation === 0 ? data.probation.head : data.probation.head.previous;
434433

435434
const hasProbation = probation !== data.probation.head;
436-
const hasEvicted = evicted !== data.probation.head;
435+
const hasEvicted = evictedCandidate !== data.probation.head;
437436

438437
let toRemove: BoundedNode<K, V>;
439438
if(! hasProbation && ! hasEvicted) {
@@ -442,20 +441,46 @@ export class BoundedCache<K extends KeyType, V> extends AbstractCache<K, V> impl
442441
} else if(! hasEvicted) {
443442
toRemove = probation;
444443
} else if(! hasProbation) {
445-
toRemove = evicted;
444+
toRemove = evictedCandidate;
446445

447446
evictedToProbation--;
448447
} else {
449-
// Estimate how often the two items have been accessed
450-
const freqEvicted = data.sketch.estimate(evicted.hashCode);
448+
/*
449+
* Estimate how often the two items have been accessed to
450+
* determine which of the keys should actually be evicted.
451+
*
452+
* Also protect against hash collision attacks where the
453+
* frequency of an item in the cache is raised causing the
454+
* candidate to never be admitted into the cache.
455+
*/
456+
let removeCandidate;
457+
458+
const freqEvictedCandidate = data.sketch.estimate(evictedCandidate.hashCode);
451459
const freqProbation = data.sketch.estimate(probation.hashCode);
452460

453-
if(freqEvicted > freqProbation) {
454-
toRemove = probation;
461+
if(freqEvictedCandidate > freqProbation) {
462+
removeCandidate = false;
463+
} else if(freqEvictedCandidate < data.sketch.slightlyLessThanHalfMaxSize) {
464+
/*
465+
* If the frequency of the candidate is slightly less than
466+
* half it can be admitted without going through randomness
467+
* checks.
468+
*
469+
* The idea here is that will reduce the number of random
470+
* admittances.
471+
*/
472+
removeCandidate = true;
455473
} else {
456-
toRemove = evicted;
474+
/*
475+
* Make it a 1 in 1000 chance that the candidate is not
476+
* removed.
477+
*
478+
* TODO: Should this be lower or higher? Please open an issue if you have thoughts on this
479+
*/
480+
removeCandidate = Math.floor(Math.random() * 1000) >= 1;
457481
}
458482

483+
toRemove = removeCandidate ? evictedCandidate : probation;
459484
evictedToProbation--;
460485
}
461486

src/cache/bounded/sketch.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,30 +28,33 @@ function safeishMultiply(a: number, b: number) {
2828
* have been made.
2929
*/
3030
export class CountMinSketch {
31-
private width: number;
32-
private depth: number;
33-
private maxSize: number;
31+
private readonly width: number;
32+
private readonly depth: number;
33+
34+
public readonly maxSize: number;
35+
public readonly halfMaxSize: number;
36+
public readonly slightlyLessThanHalfMaxSize: number;
3437

3538
private additions: number;
3639
private resetAfter: number;
3740

3841
private table: Uint8Array;
39-
private random: number;
4042

4143
constructor(width: number, depth: number, decay: boolean) {
4244
this.width = toPowerOfN(width);
4345
this.depth = depth;
4446

4547
// Get the maximum size of values, assuming unsigned ints
4648
this.maxSize = Math.pow(2, Uint8Array.BYTES_PER_ELEMENT * 8) - 1;
49+
this.halfMaxSize = this.maxSize / 2;
50+
this.slightlyLessThanHalfMaxSize = this.halfMaxSize - Math.max(this.halfMaxSize / 4, 1);
4751

4852
// Track additions and when to reset
4953
this.additions = 0;
5054
this.resetAfter = decay ? width * 10 : -1;
5155

5256
// Create the table to store data in
5357
this.table = new Uint8Array(this.width * depth);
54-
this.random = Math.floor(Math.random() * 0xffffff) | 1;
5558
}
5659

5760
private findIndex(h1: number, h2: number, d: number) {

0 commit comments

Comments
 (0)