@angular-helpers/security 21.4.2 → 21.4.3

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.
@@ -922,6 +922,7 @@ const DEFAULT_ALLOWED_TAGS = [
922
922
  const DEFAULT_ALLOWED_ATTRIBUTES = {
923
923
  a: ['href'],
924
924
  };
925
+ const URL_ATTRIBUTES = new Set(['href', 'src', 'action', 'formaction', 'data', 'poster']);
925
926
  /**
926
927
  * Sanitizes an HTML string by allowlist. Browser-only: requires DOMParser.
927
928
  *
@@ -980,7 +981,7 @@ function sanitizeElementAttributes(element, tagName, allowedAttributes) {
980
981
  attrsToRemove.push(attr.name);
981
982
  continue;
982
983
  }
983
- if (attr.name === 'href' && sanitizeUrlString(attr.value) === null) {
984
+ if (URL_ATTRIBUTES.has(attr.name) && sanitizeUrlString(attr.value) === null) {
984
985
  attrsToRemove.push(attr.name);
985
986
  }
986
987
  }
@@ -1512,13 +1513,23 @@ class RateLimiterService {
1512
1513
  this.configure(key, policy);
1513
1514
  }
1514
1515
  }
1515
- this.destroyRef.onDestroy(() => this.buckets.clear());
1516
+ this.destroyRef.onDestroy(() => {
1517
+ for (const bucket of this.buckets.values()) {
1518
+ if (bucket.timerId)
1519
+ clearTimeout(bucket.timerId);
1520
+ }
1521
+ this.buckets.clear();
1522
+ });
1516
1523
  }
1517
1524
  /**
1518
1525
  * Registers or updates the policy for `key`. Re-configuring an existing key resets its state.
1519
1526
  */
1520
1527
  configure(key, policy) {
1521
1528
  validatePolicy(policy);
1529
+ const existing = this.buckets.get(key);
1530
+ if (existing && existing.timerId) {
1531
+ clearTimeout(existing.timerId);
1532
+ }
1522
1533
  const now = Date.now();
1523
1534
  const initialRemaining = policy.type === 'token-bucket' ? policy.capacity : policy.max;
1524
1535
  this.buckets.set(key, {
@@ -1553,6 +1564,7 @@ class RateLimiterService {
1553
1564
  }
1554
1565
  bucket.tokens -= tokens;
1555
1566
  bucket.remaining.set(Math.floor(bucket.tokens));
1567
+ this.scheduleAutoRefill(key, bucket);
1556
1568
  return;
1557
1569
  }
1558
1570
  // sliding-window
@@ -1566,6 +1578,7 @@ class RateLimiterService {
1566
1578
  for (let i = 0; i < tokens; i++)
1567
1579
  bucket.timestamps.push(now);
1568
1580
  bucket.remaining.set(bucket.policy.max - bucket.timestamps.length);
1581
+ this.scheduleAutoRefill(key, bucket);
1569
1582
  }
1570
1583
  /**
1571
1584
  * Reactive signal indicating whether a single unit can be consumed from `key` right now.
@@ -1594,6 +1607,10 @@ class RateLimiterService {
1594
1607
  const bucket = this.buckets.get(key);
1595
1608
  if (!bucket)
1596
1609
  return;
1610
+ if (bucket.timerId) {
1611
+ clearTimeout(bucket.timerId);
1612
+ bucket.timerId = undefined;
1613
+ }
1597
1614
  bucket.timestamps = [];
1598
1615
  if (bucket.policy.type === 'token-bucket') {
1599
1616
  bucket.tokens = bucket.policy.capacity;
@@ -1615,6 +1632,41 @@ class RateLimiterService {
1615
1632
  bucket.lastRefillAt = now;
1616
1633
  bucket.remaining.set(Math.floor(bucket.tokens));
1617
1634
  }
1635
+ scheduleAutoRefill(key, bucket) {
1636
+ if (bucket.timerId) {
1637
+ clearTimeout(bucket.timerId);
1638
+ bucket.timerId = undefined;
1639
+ }
1640
+ const now = Date.now();
1641
+ if (bucket.policy.type === 'token-bucket') {
1642
+ if (bucket.tokens >= bucket.policy.capacity) {
1643
+ return;
1644
+ }
1645
+ const currentFloor = Math.floor(bucket.tokens);
1646
+ const nextInteger = currentFloor + 1;
1647
+ const deficit = nextInteger - bucket.tokens;
1648
+ const timeToNextTokenMs = Math.max(10, Math.ceil((deficit / bucket.policy.refillPerSecond) * 1000));
1649
+ bucket.timerId = setTimeout(() => {
1650
+ const tNow = Date.now();
1651
+ this.refillTokenBucket(bucket, tNow);
1652
+ this.scheduleAutoRefill(key, bucket);
1653
+ }, timeToNextTokenMs);
1654
+ }
1655
+ else {
1656
+ // sliding-window
1657
+ const windowStart = now - bucket.policy.windowMs;
1658
+ bucket.timestamps = bucket.timestamps.filter((t) => t > windowStart);
1659
+ bucket.remaining.set(bucket.policy.max - bucket.timestamps.length);
1660
+ if (bucket.timestamps.length === 0) {
1661
+ return;
1662
+ }
1663
+ const oldest = bucket.timestamps[0];
1664
+ const timeToExpiryMs = Math.max(10, oldest + bucket.policy.windowMs - now);
1665
+ bucket.timerId = setTimeout(() => {
1666
+ this.scheduleAutoRefill(key, bucket);
1667
+ }, timeToExpiryMs);
1668
+ }
1669
+ }
1618
1670
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RateLimiterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1619
1671
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RateLimiterService });
1620
1672
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-helpers/security",
3
- "version": "21.4.2",
3
+ "version": "21.4.3",
4
4
  "description": "Angular security helpers for preventing ReDoS and other security vulnerabilities",
5
5
  "keywords": [
6
6
  "angular",
@@ -675,6 +675,7 @@ declare class RateLimiterService {
675
675
  */
676
676
  reset(key: string): void;
677
677
  private refillTokenBucket;
678
+ private scheduleAutoRefill;
678
679
  static ɵfac: i0.ɵɵFactoryDeclaration<RateLimiterService, never>;
679
680
  static ɵprov: i0.ɵɵInjectableDeclaration<RateLimiterService>;
680
681
  }