@bquery/bquery 1.1.0 → 1.1.2
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 +3 -0
- package/dist/full.iife.js +1 -1
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +1 -1
- package/dist/full.umd.js.map +1 -1
- package/dist/security/sanitize.d.ts.map +1 -1
- package/dist/security.es.mjs +95 -74
- package/dist/security.es.mjs.map +1 -1
- package/package.json +120 -120
- package/src/security/sanitize.ts +72 -0
package/src/security/sanitize.ts
CHANGED
|
@@ -261,11 +261,13 @@ const DEFAULT_ALLOWED_ATTRIBUTES = new Set([
|
|
|
261
261
|
'lang',
|
|
262
262
|
'loading',
|
|
263
263
|
'name',
|
|
264
|
+
'rel',
|
|
264
265
|
'role',
|
|
265
266
|
'src',
|
|
266
267
|
'srcset',
|
|
267
268
|
'style',
|
|
268
269
|
'tabindex',
|
|
270
|
+
'target',
|
|
269
271
|
'title',
|
|
270
272
|
'type',
|
|
271
273
|
'width',
|
|
@@ -351,6 +353,54 @@ const isSafeUrl = (value: string): boolean => {
|
|
|
351
353
|
return true;
|
|
352
354
|
};
|
|
353
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Check if a URL is external (different origin).
|
|
358
|
+
* @internal
|
|
359
|
+
*/
|
|
360
|
+
const isExternalUrl = (url: string): boolean => {
|
|
361
|
+
try {
|
|
362
|
+
// Normalize URL by trimming whitespace
|
|
363
|
+
const trimmedUrl = url.trim();
|
|
364
|
+
|
|
365
|
+
// Protocol-relative URLs (//example.com) are always external.
|
|
366
|
+
// CRITICAL: This check must run before the relative-URL check below;
|
|
367
|
+
// otherwise, a protocol-relative URL like "//evil.com" would be treated
|
|
368
|
+
// as a non-http(s) relative URL and incorrectly classified as same-origin.
|
|
369
|
+
// Handling them up front guarantees correct security classification.
|
|
370
|
+
if (trimmedUrl.startsWith('//')) {
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Normalize URL for case-insensitive protocol checks
|
|
375
|
+
const lowerUrl = trimmedUrl.toLowerCase();
|
|
376
|
+
|
|
377
|
+
// Check for non-http(s) protocols which are considered external/special
|
|
378
|
+
// (mailto:, tel:, ftp:, etc.)
|
|
379
|
+
const hasProtocol = /^[a-z][a-z0-9+.-]*:/i.test(trimmedUrl);
|
|
380
|
+
if (hasProtocol && !lowerUrl.startsWith('http://') && !lowerUrl.startsWith('https://')) {
|
|
381
|
+
// These are special protocols, not traditional "external" links
|
|
382
|
+
// but we treat them as external for security consistency
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Relative URLs are not external
|
|
387
|
+
if (!lowerUrl.startsWith('http://') && !lowerUrl.startsWith('https://')) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// In non-browser environments (e.g., Node.js), treat all absolute URLs as external
|
|
392
|
+
if (typeof window === 'undefined' || !window.location) {
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const urlObj = new URL(trimmedUrl, window.location.href);
|
|
397
|
+
return urlObj.origin !== window.location.origin;
|
|
398
|
+
} catch {
|
|
399
|
+
// If URL parsing fails, treat as potentially external for safety
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
354
404
|
/**
|
|
355
405
|
* Core sanitization logic (without Trusted Types wrapper).
|
|
356
406
|
* @internal
|
|
@@ -433,6 +483,28 @@ const sanitizeHtmlCore = (html: string, options: SanitizeOptions = {}): string =
|
|
|
433
483
|
for (const attrName of attrsToRemove) {
|
|
434
484
|
el.removeAttribute(attrName);
|
|
435
485
|
}
|
|
486
|
+
|
|
487
|
+
// Add rel="noopener noreferrer" to external links for security
|
|
488
|
+
if (tagName === 'a') {
|
|
489
|
+
const href = el.getAttribute('href');
|
|
490
|
+
const target = el.getAttribute('target');
|
|
491
|
+
const hasTargetBlank = target?.toLowerCase() === '_blank';
|
|
492
|
+
const isExternal = href && isExternalUrl(href);
|
|
493
|
+
|
|
494
|
+
// Add security attributes to links opening in new window or external links
|
|
495
|
+
if (hasTargetBlank || isExternal) {
|
|
496
|
+
const existingRel = el.getAttribute('rel');
|
|
497
|
+
const relValues = new Set(
|
|
498
|
+
existingRel ? existingRel.split(/\s+/).filter(Boolean) : []
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
// Add noopener and noreferrer
|
|
502
|
+
relValues.add('noopener');
|
|
503
|
+
relValues.add('noreferrer');
|
|
504
|
+
|
|
505
|
+
el.setAttribute('rel', Array.from(relValues).join(' '));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
436
508
|
}
|
|
437
509
|
|
|
438
510
|
// Remove disallowed elements
|