html2canvas-pro 2.1.0 → 2.2.0
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/dist/html2canvas-pro.esm.js +10226 -10526
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +10869 -11171
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +8 -8
- package/dist/lib/config.js +0 -22
- package/dist/lib/core/cache-storage.js +3 -40
- package/dist/lib/core/constants.js +25 -0
- package/dist/lib/core/context.js +1 -0
- package/dist/lib/core/features.js +3 -2
- package/dist/lib/core/validator.js +3 -3
- package/dist/lib/css/grouped/background-styles.js +36 -0
- package/dist/lib/css/grouped/border-styles.js +75 -0
- package/dist/lib/css/grouped/font-styles.js +93 -0
- package/dist/lib/css/grouped/layout-styles.js +127 -0
- package/dist/lib/css/index.js +74 -46
- package/dist/lib/css/layout/text.js +7 -6
- package/dist/lib/css/property-descriptors/background-blend-mode.js +41 -0
- package/dist/lib/css/property-descriptors/border-image-repeat.js +42 -0
- package/dist/lib/css/property-descriptors/border-image-slice.js +45 -0
- package/dist/lib/css/property-descriptors/border-image-source.js +21 -0
- package/dist/lib/css/property-descriptors/border-radius.js +1 -1
- package/dist/lib/css/property-descriptors/box-decoration-break.js +18 -0
- package/dist/lib/css/property-descriptors/counter-increment.js +17 -12
- package/dist/lib/css/property-descriptors/counter-reset.js +4 -12
- package/dist/lib/css/property-descriptors/filter.js +76 -0
- package/dist/lib/css/property-descriptors/font-variant-ligatures.js +34 -0
- package/dist/lib/css/property-descriptors/object-fit.js +1 -1
- package/dist/lib/css/property-descriptors/object-position.js +42 -0
- package/dist/lib/css/property-descriptors/visibility.js +1 -1
- package/dist/lib/css/property-descriptors/zoom.js +18 -0
- package/dist/lib/css/syntax/parser.js +0 -1
- package/dist/lib/css/types/color.js +5 -1
- package/dist/lib/css/types/functions/repeating-linear-gradient.js +9 -0
- package/dist/lib/css/types/image.js +12 -2
- package/dist/lib/css/types/length-percentage.js +6 -2
- package/dist/lib/css/types/safe-eval.js +80 -0
- package/dist/lib/dom/document-cloner.js +23 -163
- package/dist/lib/dom/slot-cloner.js +176 -0
- package/dist/lib/index.js +1 -17
- package/dist/lib/render/canvas/background-renderer.js +169 -30
- package/dist/lib/render/canvas/border-image-renderer.js +153 -0
- package/dist/lib/render/canvas/canvas-renderer.js +39 -190
- package/dist/lib/render/canvas/content-renderer.js +202 -0
- package/dist/lib/render/canvas/effects-renderer.js +3 -0
- package/dist/lib/render/canvas/foreignobject-renderer.js +5 -1
- package/dist/lib/render/canvas/text/text-decoration-renderer.js +99 -0
- package/dist/lib/render/canvas/text-renderer.js +100 -224
- package/dist/lib/render/effects.js +38 -3
- package/dist/lib/render/object-fit.js +19 -15
- package/dist/lib/render/stacking-context.js +11 -0
- package/dist/types/config.d.ts +0 -10
- package/dist/types/core/cache-storage.d.ts +0 -24
- package/dist/types/core/constants.d.ts +22 -0
- package/dist/types/core/context.d.ts +3 -0
- package/dist/types/core/performance-monitor.d.ts +4 -4
- package/dist/types/core/validator.d.ts +6 -8
- package/dist/types/css/grouped/background-styles.d.ts +16 -0
- package/dist/types/css/grouped/border-styles.d.ts +31 -0
- package/dist/types/css/grouped/font-styles.d.ts +35 -0
- package/dist/types/css/grouped/layout-styles.d.ts +46 -0
- package/dist/types/css/index.d.ts +30 -0
- package/dist/types/css/property-descriptors/background-blend-mode.d.ts +23 -0
- package/dist/types/css/property-descriptors/border-image-repeat.d.ts +12 -0
- package/dist/types/css/property-descriptors/border-image-slice.d.ts +10 -0
- package/dist/types/css/property-descriptors/border-image-source.d.ts +4 -0
- package/dist/types/css/property-descriptors/box-decoration-break.d.ts +6 -0
- package/dist/types/css/property-descriptors/counter-increment.d.ts +3 -0
- package/dist/types/css/property-descriptors/filter.d.ts +3 -0
- package/dist/types/css/property-descriptors/font-variant-ligatures.d.ts +14 -0
- package/dist/types/css/property-descriptors/object-position.d.ts +4 -0
- package/dist/types/css/property-descriptors/zoom.d.ts +3 -0
- package/dist/types/css/types/functions/repeating-linear-gradient.d.ts +4 -0
- package/dist/types/css/types/image.d.ts +4 -2
- package/dist/types/css/types/safe-eval.d.ts +8 -0
- package/dist/types/dom/document-cloner.d.ts +3 -44
- package/dist/types/dom/slot-cloner.d.ts +66 -0
- package/dist/types/index.d.ts +3 -7
- package/dist/types/options.d.ts +11 -0
- package/dist/types/render/canvas/background-renderer.d.ts +23 -0
- package/dist/types/render/canvas/border-image-renderer.d.ts +18 -0
- package/dist/types/render/canvas/canvas-renderer.d.ts +1 -0
- package/dist/types/render/canvas/content-renderer.d.ts +44 -0
- package/dist/types/render/canvas/text/text-decoration-renderer.d.ts +18 -0
- package/dist/types/render/canvas/text-renderer.d.ts +12 -1
- package/dist/types/render/effects.d.ts +12 -2
- package/dist/types/render/object-fit.d.ts +2 -1
- package/dist/types/render/renderer-interface.d.ts +11 -9
- package/package.json +7 -20
- package/dist/lib/dom/replaced-elements/pseudo-elements.js +0 -0
- package/dist/lib/invariant.js +0 -9
- package/dist/types/dom/replaced-elements/pseudo-elements.d.ts +0 -0
- package/dist/types/invariant.d.ts +0 -1
- package/src/__tests__/index.ts +0 -99
- package/src/config.ts +0 -107
- package/src/core/__mocks__/cache-storage.ts +0 -1
- package/src/core/__mocks__/context.ts +0 -19
- package/src/core/__mocks__/features.ts +0 -8
- package/src/core/__mocks__/logger.ts +0 -17
- package/src/core/__tests__/cache-storage.test.ts +0 -205
- package/src/core/__tests__/cache-storage.ts +0 -278
- package/src/core/__tests__/logger.ts +0 -29
- package/src/core/__tests__/validator.ts +0 -359
- package/src/core/bitwise.ts +0 -1
- package/src/core/cache-storage.ts +0 -315
- package/src/core/context.ts +0 -31
- package/src/core/debugger.ts +0 -32
- package/src/core/features.ts +0 -222
- package/src/core/logger.ts +0 -64
- package/src/core/origin-checker.ts +0 -57
- package/src/core/performance-monitor.ts +0 -241
- package/src/core/render-element.ts +0 -272
- package/src/core/util.ts +0 -1
- package/src/core/validator.ts +0 -593
- package/src/css/index.ts +0 -427
- package/src/css/layout/__mocks__/bounds.ts +0 -6
- package/src/css/layout/bounds.ts +0 -79
- package/src/css/layout/text.ts +0 -161
- package/src/css/property-descriptor.ts +0 -49
- package/src/css/property-descriptors/__tests__/background-tests.ts +0 -65
- package/src/css/property-descriptors/__tests__/clip-path.test.ts +0 -280
- package/src/css/property-descriptors/__tests__/font-family.ts +0 -25
- package/src/css/property-descriptors/__tests__/image-rendering-integration.test.ts +0 -153
- package/src/css/property-descriptors/__tests__/image-rendering-performance.test.ts +0 -175
- package/src/css/property-descriptors/__tests__/image-rendering.test.ts +0 -72
- package/src/css/property-descriptors/__tests__/paint-order.ts +0 -87
- package/src/css/property-descriptors/__tests__/text-shadow.ts +0 -94
- package/src/css/property-descriptors/__tests__/transform-tests.ts +0 -18
- package/src/css/property-descriptors/background-clip.ts +0 -30
- package/src/css/property-descriptors/background-color.ts +0 -9
- package/src/css/property-descriptors/background-image.ts +0 -27
- package/src/css/property-descriptors/background-origin.ts +0 -31
- package/src/css/property-descriptors/background-position.ts +0 -38
- package/src/css/property-descriptors/background-repeat.ts +0 -44
- package/src/css/property-descriptors/background-size.ts +0 -27
- package/src/css/property-descriptors/border-color.ts +0 -13
- package/src/css/property-descriptors/border-radius.ts +0 -19
- package/src/css/property-descriptors/border-style.ts +0 -34
- package/src/css/property-descriptors/border-width.ts +0 -20
- package/src/css/property-descriptors/box-shadow.ts +0 -60
- package/src/css/property-descriptors/clip-path.ts +0 -271
- package/src/css/property-descriptors/color.ts +0 -9
- package/src/css/property-descriptors/content.ts +0 -26
- package/src/css/property-descriptors/counter-increment.ts +0 -43
- package/src/css/property-descriptors/counter-reset.ts +0 -36
- package/src/css/property-descriptors/direction.ts +0 -23
- package/src/css/property-descriptors/display.ts +0 -117
- package/src/css/property-descriptors/duration.ts +0 -14
- package/src/css/property-descriptors/float.ts +0 -29
- package/src/css/property-descriptors/font-family.ts +0 -38
- package/src/css/property-descriptors/font-size.ts +0 -9
- package/src/css/property-descriptors/font-style.ts +0 -25
- package/src/css/property-descriptors/font-variant.ts +0 -12
- package/src/css/property-descriptors/font-weight.ts +0 -26
- package/src/css/property-descriptors/image-rendering.ts +0 -33
- package/src/css/property-descriptors/letter-spacing.ts +0 -25
- package/src/css/property-descriptors/line-break.ts +0 -22
- package/src/css/property-descriptors/line-height.ts +0 -22
- package/src/css/property-descriptors/list-style-image.ts +0 -19
- package/src/css/property-descriptors/list-style-position.ts +0 -22
- package/src/css/property-descriptors/list-style-type.ts +0 -179
- package/src/css/property-descriptors/margin.ts +0 -13
- package/src/css/property-descriptors/mix-blend-mode.ts +0 -35
- package/src/css/property-descriptors/object-fit.ts +0 -39
- package/src/css/property-descriptors/opacity.ts +0 -15
- package/src/css/property-descriptors/overflow-wrap.ts +0 -22
- package/src/css/property-descriptors/overflow.ts +0 -34
- package/src/css/property-descriptors/padding.ts +0 -14
- package/src/css/property-descriptors/paint-order.ts +0 -42
- package/src/css/property-descriptors/position.ts +0 -30
- package/src/css/property-descriptors/quotes.ts +0 -57
- package/src/css/property-descriptors/rotate.ts +0 -34
- package/src/css/property-descriptors/text-align.ts +0 -26
- package/src/css/property-descriptors/text-decoration-color.ts +0 -9
- package/src/css/property-descriptors/text-decoration-line.ts +0 -38
- package/src/css/property-descriptors/text-decoration-style.ts +0 -32
- package/src/css/property-descriptors/text-decoration-thickness.ts +0 -30
- package/src/css/property-descriptors/text-overflow.ts +0 -23
- package/src/css/property-descriptors/text-shadow.ts +0 -52
- package/src/css/property-descriptors/text-transform.ts +0 -27
- package/src/css/property-descriptors/text-underline-offset.ts +0 -27
- package/src/css/property-descriptors/transform-origin.ts +0 -29
- package/src/css/property-descriptors/transform.ts +0 -74
- package/src/css/property-descriptors/visibility.ts +0 -25
- package/src/css/property-descriptors/webkit-line-clamp.ts +0 -30
- package/src/css/property-descriptors/webkit-text-stroke-color.ts +0 -8
- package/src/css/property-descriptors/webkit-text-stroke-width.ts +0 -15
- package/src/css/property-descriptors/word-break.ts +0 -25
- package/src/css/property-descriptors/writing-mode.ts +0 -37
- package/src/css/property-descriptors/z-index.ts +0 -27
- package/src/css/syntax/__tests__/tokernizer-tests.ts +0 -29
- package/src/css/syntax/parser.ts +0 -188
- package/src/css/syntax/tokenizer.ts +0 -822
- package/src/css/type-descriptor.ts +0 -7
- package/src/css/types/__tests__/color-tests.ts +0 -147
- package/src/css/types/__tests__/image-tests.ts +0 -239
- package/src/css/types/angle.ts +0 -86
- package/src/css/types/color-math.ts +0 -22
- package/src/css/types/color-spaces/a98.ts +0 -86
- package/src/css/types/color-spaces/p3.ts +0 -92
- package/src/css/types/color-spaces/pro-photo.ts +0 -87
- package/src/css/types/color-spaces/rec2020.ts +0 -90
- package/src/css/types/color-spaces/srgb.ts +0 -87
- package/src/css/types/color-utilities.ts +0 -452
- package/src/css/types/color.ts +0 -485
- package/src/css/types/functions/-prefix-linear-gradient.ts +0 -35
- package/src/css/types/functions/-prefix-radial-gradient.ts +0 -106
- package/src/css/types/functions/-webkit-gradient.ts +0 -69
- package/src/css/types/functions/__tests__/radial-gradient.ts +0 -69
- package/src/css/types/functions/counter.ts +0 -511
- package/src/css/types/functions/gradient.ts +0 -206
- package/src/css/types/functions/linear-gradient.ts +0 -28
- package/src/css/types/functions/radial-gradient.ts +0 -101
- package/src/css/types/image.ts +0 -120
- package/src/css/types/index.ts +0 -1
- package/src/css/types/length-percentage.ts +0 -137
- package/src/css/types/length.ts +0 -7
- package/src/css/types/time.ts +0 -20
- package/src/dom/__mocks__/document-cloner.ts +0 -22
- package/src/dom/__tests__/dom-normalizer.test.ts +0 -133
- package/src/dom/__tests__/element-container.test.ts +0 -129
- package/src/dom/document-cloner.ts +0 -929
- package/src/dom/dom-normalizer.ts +0 -133
- package/src/dom/element-container.ts +0 -75
- package/src/dom/elements/li-element-container.ts +0 -10
- package/src/dom/elements/ol-element-container.ts +0 -12
- package/src/dom/elements/select-element-container.ts +0 -10
- package/src/dom/elements/textarea-element-container.ts +0 -9
- package/src/dom/node-parser.ts +0 -177
- package/src/dom/node-type-guards.ts +0 -70
- package/src/dom/replaced-elements/canvas-element-container.ts +0 -15
- package/src/dom/replaced-elements/iframe-element-container.ts +0 -55
- package/src/dom/replaced-elements/image-element-container.ts +0 -16
- package/src/dom/replaced-elements/index.ts +0 -5
- package/src/dom/replaced-elements/input-element-container.ts +0 -105
- package/src/dom/replaced-elements/pseudo-elements.ts +0 -0
- package/src/dom/replaced-elements/svg-element-container.ts +0 -23
- package/src/dom/text-container.ts +0 -42
- package/src/global.d.ts +0 -19
- package/src/index.ts +0 -82
- package/src/invariant.ts +0 -5
- package/src/options.ts +0 -55
- package/src/render/__tests__/object-fit.test.ts +0 -85
- package/src/render/background.ts +0 -298
- package/src/render/bezier-curve.ts +0 -47
- package/src/render/border.ts +0 -165
- package/src/render/bound-curves.ts +0 -388
- package/src/render/box-sizing.ts +0 -31
- package/src/render/canvas/__tests__/background-renderer.test.ts +0 -72
- package/src/render/canvas/__tests__/border-renderer.test.ts +0 -24
- package/src/render/canvas/__tests__/effects-renderer.test.ts +0 -32
- package/src/render/canvas/__tests__/text-renderer.test.ts +0 -471
- package/src/render/canvas/background-renderer.ts +0 -271
- package/src/render/canvas/border-renderer.ts +0 -224
- package/src/render/canvas/canvas-path.ts +0 -31
- package/src/render/canvas/canvas-renderer.ts +0 -641
- package/src/render/canvas/effects-renderer.ts +0 -130
- package/src/render/canvas/foreignobject-renderer.ts +0 -53
- package/src/render/canvas/text-renderer.ts +0 -700
- package/src/render/effects.ts +0 -75
- package/src/render/font-metrics.ts +0 -72
- package/src/render/object-fit.ts +0 -100
- package/src/render/path.ts +0 -37
- package/src/render/renderer-interface.ts +0 -28
- package/src/render/stacking-context.ts +0 -386
- package/src/render/vector.ts +0 -19
package/src/core/validator.ts
DELETED
|
@@ -1,593 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Input Validator
|
|
3
|
-
*
|
|
4
|
-
* Provides validation and sanitization for user inputs to prevent security vulnerabilities
|
|
5
|
-
* including SSRF, XSS, and injection attacks.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Validation result
|
|
10
|
-
*/
|
|
11
|
-
export interface ValidationResult {
|
|
12
|
-
valid: boolean;
|
|
13
|
-
error?: string;
|
|
14
|
-
sanitized?: any;
|
|
15
|
-
/**
|
|
16
|
-
* Indicates if runtime validation is recommended
|
|
17
|
-
* (e.g., for proxy URLs to prevent DNS rebinding attacks)
|
|
18
|
-
*/
|
|
19
|
-
requiresRuntimeCheck?: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Validator configuration
|
|
24
|
-
*/
|
|
25
|
-
export interface ValidatorConfig {
|
|
26
|
-
/**
|
|
27
|
-
* Allowed proxy domains for SSRF prevention
|
|
28
|
-
* If empty, no domain restrictions
|
|
29
|
-
*/
|
|
30
|
-
allowedProxyDomains?: string[];
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Allow localhost/127.0.0.1 as proxy URL. Only set for development or test (e.g. Karma reftests).
|
|
34
|
-
* @default false
|
|
35
|
-
*/
|
|
36
|
-
allowLocalhostProxy?: boolean;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Maximum allowed image timeout in milliseconds
|
|
40
|
-
*/
|
|
41
|
-
maxImageTimeout?: number;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Whether to allow data URLs
|
|
45
|
-
*/
|
|
46
|
-
allowDataUrls?: boolean;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Custom validation function
|
|
50
|
-
*/
|
|
51
|
-
customValidator?: (value: any, type: string) => ValidationResult;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Input Validator
|
|
56
|
-
*
|
|
57
|
-
* Validates and sanitizes user inputs for security and correctness.
|
|
58
|
-
*/
|
|
59
|
-
export class Validator {
|
|
60
|
-
private readonly config: ValidatorConfig;
|
|
61
|
-
|
|
62
|
-
constructor(config: ValidatorConfig = {}) {
|
|
63
|
-
this.config = {
|
|
64
|
-
maxImageTimeout: 300000, // 5 minutes default
|
|
65
|
-
allowDataUrls: true,
|
|
66
|
-
...config
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Validate a URL
|
|
72
|
-
*
|
|
73
|
-
* @param url - URL to validate
|
|
74
|
-
* @param context - Context for validation (e.g., 'proxy', 'image')
|
|
75
|
-
* @returns Validation result
|
|
76
|
-
*/
|
|
77
|
-
validateUrl(url: string, context: 'proxy' | 'image' | 'general' = 'general'): ValidationResult {
|
|
78
|
-
if (!url || typeof url !== 'string') {
|
|
79
|
-
return {
|
|
80
|
-
valid: false,
|
|
81
|
-
error: 'URL must be a non-empty string'
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Check for data URLs
|
|
86
|
-
if (url.startsWith('data:')) {
|
|
87
|
-
if (!this.config.allowDataUrls) {
|
|
88
|
-
return {
|
|
89
|
-
valid: false,
|
|
90
|
-
error: 'Data URLs are not allowed'
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
return { valid: true, sanitized: url };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Check for blob URLs
|
|
97
|
-
if (url.startsWith('blob:')) {
|
|
98
|
-
return { valid: true, sanitized: url };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Validate URL format
|
|
102
|
-
try {
|
|
103
|
-
const parsedUrl = new URL(url);
|
|
104
|
-
|
|
105
|
-
// Only allow http and https protocols
|
|
106
|
-
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
107
|
-
return {
|
|
108
|
-
valid: false,
|
|
109
|
-
error: `Protocol ${parsedUrl.protocol} is not allowed. Only http and https are permitted.`
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// For proxy URLs, check domain whitelist
|
|
114
|
-
if (context === 'proxy' && this.config.allowedProxyDomains && this.config.allowedProxyDomains.length > 0) {
|
|
115
|
-
const hostname = parsedUrl.hostname.toLowerCase();
|
|
116
|
-
const isAllowed = this.config.allowedProxyDomains.some((domain) => {
|
|
117
|
-
const normalizedDomain = domain.toLowerCase();
|
|
118
|
-
return hostname === normalizedDomain || hostname.endsWith('.' + normalizedDomain);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
if (!isAllowed) {
|
|
122
|
-
return {
|
|
123
|
-
valid: false,
|
|
124
|
-
error: `Proxy domain ${parsedUrl.hostname} is not in the allowed list`
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Check for localhost/private IPs to prevent SSRF (skip when allowLocalhostProxy for dev/test)
|
|
130
|
-
if (context === 'proxy') {
|
|
131
|
-
if (!this.config.allowLocalhostProxy) {
|
|
132
|
-
const hostname = parsedUrl.hostname.toLowerCase();
|
|
133
|
-
|
|
134
|
-
// Check for localhost
|
|
135
|
-
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
|
|
136
|
-
return {
|
|
137
|
-
valid: false,
|
|
138
|
-
error: 'Localhost is not allowed for proxy URLs'
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// For private IP ranges (simplified check)
|
|
143
|
-
if (this.isPrivateIP(hostname)) {
|
|
144
|
-
return {
|
|
145
|
-
valid: false,
|
|
146
|
-
error: 'Private IP addresses are not allowed for proxy URLs'
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// For link-local addresses
|
|
151
|
-
if (hostname.startsWith('169.254.') || hostname.startsWith('fe80:')) {
|
|
152
|
-
return {
|
|
153
|
-
valid: false,
|
|
154
|
-
error: 'Link-local addresses are not allowed for proxy URLs'
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// For proxy URLs, mark that runtime validation is recommended
|
|
160
|
-
// to prevent DNS rebinding attacks
|
|
161
|
-
return {
|
|
162
|
-
valid: true,
|
|
163
|
-
sanitized: url,
|
|
164
|
-
requiresRuntimeCheck: true
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return { valid: true, sanitized: url };
|
|
169
|
-
} catch (e) {
|
|
170
|
-
return {
|
|
171
|
-
valid: false,
|
|
172
|
-
error: `Invalid URL format: ${e instanceof Error ? e.message : 'Unknown error'}`
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Check if a hostname is a private IP address
|
|
179
|
-
*/
|
|
180
|
-
private isPrivateIP(hostname: string): boolean {
|
|
181
|
-
// IPv4 private ranges
|
|
182
|
-
const privateIPv4Patterns = [
|
|
183
|
-
/^0\./, // 0.0.0.0/8 (This network)
|
|
184
|
-
/^10\./, // 10.0.0.0/8 (Private)
|
|
185
|
-
/^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./, // 100.64.0.0/10 (CGNAT)
|
|
186
|
-
/^127\./, // 127.0.0.0/8 (Loopback)
|
|
187
|
-
/^169\.254\./, // 169.254.0.0/16 (Link-local)
|
|
188
|
-
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12 (Private)
|
|
189
|
-
/^192\.0\.0\./, // 192.0.0.0/24 (IETF Protocol Assignments)
|
|
190
|
-
/^192\.0\.2\./, // 192.0.2.0/24 (TEST-NET-1)
|
|
191
|
-
/^192\.168\./, // 192.168.0.0/16 (Private)
|
|
192
|
-
/^198\.(1[8-9])\./, // 198.18.0.0/15 (Network benchmark)
|
|
193
|
-
/^198\.51\.100\./, // 198.51.100.0/24 (TEST-NET-2)
|
|
194
|
-
/^203\.0\.113\./, // 203.0.113.0/24 (TEST-NET-3)
|
|
195
|
-
/^2(2[4-9]|3[0-9])\./, // 224.0.0.0/4 (Multicast)
|
|
196
|
-
/^24[0-9]\./, // 240.0.0.0/4 (Reserved)
|
|
197
|
-
/^255\.255\.255\.255$/ // 255.255.255.255/32 (Broadcast)
|
|
198
|
-
];
|
|
199
|
-
|
|
200
|
-
// Check IPv4
|
|
201
|
-
if (privateIPv4Patterns.some((pattern) => pattern.test(hostname))) {
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// IPv6 private ranges and special addresses
|
|
206
|
-
if (hostname.includes(':')) {
|
|
207
|
-
return this.isPrivateIPv6(hostname);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Check if an IPv6 address is private or special
|
|
215
|
-
* Handles compressed IPv6 addresses (e.g., ::1, fc00::1)
|
|
216
|
-
*/
|
|
217
|
-
private isPrivateIPv6(hostname: string): boolean {
|
|
218
|
-
const normalizedHost = hostname.toLowerCase().trim();
|
|
219
|
-
// Remove square brackets if present (e.g., [::1])
|
|
220
|
-
const addr = normalizedHost.replace(/^\[|\]$/g, '');
|
|
221
|
-
// Remove zone ID if present (e.g., fe80::1%eth0)
|
|
222
|
-
const addrWithoutZone = addr.split('%')[0];
|
|
223
|
-
// Loopback ::1 (also matches 0:0:0:0:0:0:0:1)
|
|
224
|
-
if (/^(0:){7}1$/.test(addrWithoutZone) || addrWithoutZone === '::1') {
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
// Unspecified address :: (also matches 0:0:0:0:0:0:0:0)
|
|
228
|
-
if (/^(0:){7}0$/.test(addrWithoutZone) || addrWithoutZone === '::') {
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
// Expand :: compression to check prefixes
|
|
232
|
-
// This handles cases like fc00::1, fe80::, etc.
|
|
233
|
-
const expandedAddr = this.expandIPv6(addrWithoutZone);
|
|
234
|
-
if (!expandedAddr) {
|
|
235
|
-
// If we can't expand it, fall back to prefix matching
|
|
236
|
-
return this.isPrivateIPv6Prefix(addrWithoutZone);
|
|
237
|
-
}
|
|
238
|
-
// fc00::/7 (Unique Local Address)
|
|
239
|
-
// Check if first byte is in range fc00-fdff
|
|
240
|
-
const firstByte = parseInt(expandedAddr.substring(0, 2), 16);
|
|
241
|
-
if (firstByte >= 0xfc && firstByte <= 0xfd) {
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
// fe80::/10 (Link-local)
|
|
245
|
-
// First 10 bits should be 1111 1110 10
|
|
246
|
-
if (firstByte === 0xfe) {
|
|
247
|
-
const secondByte = parseInt(expandedAddr.substring(2, 4), 16);
|
|
248
|
-
// Check if bits 11-12 are 10 (0x80-0xbf)
|
|
249
|
-
if (secondByte >= 0x80 && secondByte <= 0xbf) {
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
// ff00::/8 (Multicast)
|
|
254
|
-
if (firstByte === 0xff) {
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Expand compressed IPv6 address to full form
|
|
262
|
-
* e.g., "::1" -> "0000:0000:0000:0000:0000:0000:0000:0001"
|
|
263
|
-
*/
|
|
264
|
-
private expandIPv6(addr: string): string | null {
|
|
265
|
-
try {
|
|
266
|
-
// Handle :: compression
|
|
267
|
-
if (addr.includes('::')) {
|
|
268
|
-
const parts = addr.split('::');
|
|
269
|
-
if (parts.length > 2) {
|
|
270
|
-
return null; // Invalid: more than one ::
|
|
271
|
-
}
|
|
272
|
-
const leftParts = parts[0] ? parts[0].split(':') : [];
|
|
273
|
-
const rightParts = parts[1] ? parts[1].split(':') : [];
|
|
274
|
-
const missingParts = 8 - leftParts.length - rightParts.length;
|
|
275
|
-
if (missingParts < 0) {
|
|
276
|
-
return null; // Invalid
|
|
277
|
-
}
|
|
278
|
-
const middleParts = Array(missingParts).fill('0000');
|
|
279
|
-
const allParts = [...leftParts, ...middleParts, ...rightParts];
|
|
280
|
-
return allParts.map((p) => p.padStart(4, '0')).join(':');
|
|
281
|
-
} else {
|
|
282
|
-
// No compression, just normalize
|
|
283
|
-
const parts = addr.split(':');
|
|
284
|
-
if (parts.length !== 8) {
|
|
285
|
-
return null; // Invalid
|
|
286
|
-
}
|
|
287
|
-
return parts.map((p) => p.padStart(4, '0')).join(':');
|
|
288
|
-
}
|
|
289
|
-
} catch {
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Fallback prefix matching for IPv6 when expansion fails
|
|
296
|
-
*/
|
|
297
|
-
private isPrivateIPv6Prefix(addr: string): boolean {
|
|
298
|
-
// fc00::/7 (Unique Local Address)
|
|
299
|
-
if (/^fc[0-9a-f]{0,2}:?/i.test(addr) || /^fd[0-9a-f]{0,2}:?/i.test(addr)) {
|
|
300
|
-
return true;
|
|
301
|
-
}
|
|
302
|
-
// fe80::/10 (Link-local)
|
|
303
|
-
if (/^fe[89ab][0-9a-f]:?/i.test(addr)) {
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
// ff00::/8 (Multicast)
|
|
307
|
-
if (/^ff[0-9a-f]{0,2}:?/i.test(addr)) {
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Validate CSP nonce
|
|
315
|
-
*
|
|
316
|
-
* @param nonce - CSP nonce to validate
|
|
317
|
-
* @returns Validation result
|
|
318
|
-
*/
|
|
319
|
-
validateCspNonce(nonce: string): ValidationResult {
|
|
320
|
-
if (!nonce || typeof nonce !== 'string') {
|
|
321
|
-
return {
|
|
322
|
-
valid: false,
|
|
323
|
-
error: 'CSP nonce must be a non-empty string'
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Basic format validation - nonce should be base64-like
|
|
328
|
-
// Typical format: base64 string, often 32+ characters
|
|
329
|
-
if (nonce.length < 16) {
|
|
330
|
-
return {
|
|
331
|
-
valid: false,
|
|
332
|
-
error: 'CSP nonce is too short (minimum 16 characters recommended)'
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Check for suspicious characters
|
|
337
|
-
if (!/^[A-Za-z0-9+/=_-]+$/.test(nonce)) {
|
|
338
|
-
return {
|
|
339
|
-
valid: false,
|
|
340
|
-
error: 'CSP nonce contains invalid characters'
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return { valid: true, sanitized: nonce };
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Validate image timeout
|
|
349
|
-
*
|
|
350
|
-
* @param timeout - Timeout in milliseconds
|
|
351
|
-
* @returns Validation result
|
|
352
|
-
*/
|
|
353
|
-
validateImageTimeout(timeout: number): ValidationResult {
|
|
354
|
-
if (typeof timeout !== 'number' || isNaN(timeout)) {
|
|
355
|
-
return {
|
|
356
|
-
valid: false,
|
|
357
|
-
error: 'Image timeout must be a number'
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (timeout < 0) {
|
|
362
|
-
return {
|
|
363
|
-
valid: false,
|
|
364
|
-
error: 'Image timeout cannot be negative'
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (this.config.maxImageTimeout && timeout > this.config.maxImageTimeout) {
|
|
369
|
-
return {
|
|
370
|
-
valid: false,
|
|
371
|
-
error: `Image timeout ${timeout}ms exceeds maximum allowed ${this.config.maxImageTimeout}ms`
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return { valid: true, sanitized: timeout };
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Validate window dimensions
|
|
380
|
-
*
|
|
381
|
-
* @param width - Window width
|
|
382
|
-
* @param height - Window height
|
|
383
|
-
* @returns Validation result
|
|
384
|
-
*/
|
|
385
|
-
validateDimensions(width: number, height: number): ValidationResult {
|
|
386
|
-
if (typeof width !== 'number' || typeof height !== 'number') {
|
|
387
|
-
return {
|
|
388
|
-
valid: false,
|
|
389
|
-
error: 'Dimensions must be numbers'
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (isNaN(width) || isNaN(height)) {
|
|
394
|
-
return {
|
|
395
|
-
valid: false,
|
|
396
|
-
error: 'Dimensions cannot be NaN'
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (width <= 0 || height <= 0) {
|
|
401
|
-
return {
|
|
402
|
-
valid: false,
|
|
403
|
-
error: 'Dimensions must be positive'
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Reasonable maximum to prevent memory issues
|
|
408
|
-
const MAX_DIMENSION = 32767; // Common canvas limit
|
|
409
|
-
if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
|
|
410
|
-
return {
|
|
411
|
-
valid: false,
|
|
412
|
-
error: `Dimensions exceed maximum allowed (${MAX_DIMENSION}px)`
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return { valid: true, sanitized: { width, height } };
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Validate scale factor
|
|
421
|
-
*
|
|
422
|
-
* @param scale - Scale factor
|
|
423
|
-
* @returns Validation result
|
|
424
|
-
*/
|
|
425
|
-
validateScale(scale: number): ValidationResult {
|
|
426
|
-
if (typeof scale !== 'number' || isNaN(scale)) {
|
|
427
|
-
return {
|
|
428
|
-
valid: false,
|
|
429
|
-
error: 'Scale must be a number'
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (scale <= 0) {
|
|
434
|
-
return {
|
|
435
|
-
valid: false,
|
|
436
|
-
error: 'Scale must be positive'
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Reasonable scale limits
|
|
441
|
-
if (scale > 10) {
|
|
442
|
-
return {
|
|
443
|
-
valid: false,
|
|
444
|
-
error: 'Scale factor too large (maximum 10x)'
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
return { valid: true, sanitized: scale };
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Validate HTML element
|
|
453
|
-
*
|
|
454
|
-
* @param element - Element to validate
|
|
455
|
-
* @returns Validation result
|
|
456
|
-
*/
|
|
457
|
-
validateElement(element: any): ValidationResult {
|
|
458
|
-
if (!element) {
|
|
459
|
-
return {
|
|
460
|
-
valid: false,
|
|
461
|
-
error: 'Element is required'
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if (typeof element !== 'object') {
|
|
466
|
-
return {
|
|
467
|
-
valid: false,
|
|
468
|
-
error: 'Element must be an object'
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Accept real HTMLElement, or any element-like object with the minimal shape
|
|
473
|
-
// required by the implementation (ownerDocument + defaultView) for backward
|
|
474
|
-
// compatibility and test environments.
|
|
475
|
-
if (typeof HTMLElement !== 'undefined' && element instanceof HTMLElement) {
|
|
476
|
-
// Real DOM element
|
|
477
|
-
if (!element.ownerDocument) {
|
|
478
|
-
return { valid: false, error: 'Element must be attached to a document' };
|
|
479
|
-
}
|
|
480
|
-
return { valid: true };
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Duck-typing: accept object with ownerDocument and defaultView (minimal contract)
|
|
484
|
-
if (!element.ownerDocument) {
|
|
485
|
-
return {
|
|
486
|
-
valid: false,
|
|
487
|
-
error: 'Element must be attached to a document (ownerDocument required)'
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
if (!element.ownerDocument.defaultView) {
|
|
491
|
-
return {
|
|
492
|
-
valid: false,
|
|
493
|
-
error: 'Document must be attached to a window (ownerDocument.defaultView required)'
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return { valid: true };
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Validate entire options object
|
|
502
|
-
*
|
|
503
|
-
* @param options - Options to validate
|
|
504
|
-
* @returns Validation result with all errors
|
|
505
|
-
*/
|
|
506
|
-
validateOptions(options: any): ValidationResult {
|
|
507
|
-
const errors: string[] = [];
|
|
508
|
-
|
|
509
|
-
// Validate proxy URL only when a non-empty string (allow null/undefined to mean "no proxy")
|
|
510
|
-
const proxyUrl = options.proxy;
|
|
511
|
-
if (proxyUrl !== undefined && proxyUrl !== null && typeof proxyUrl === 'string' && proxyUrl.length > 0) {
|
|
512
|
-
const proxyResult = this.validateUrl(proxyUrl, 'proxy');
|
|
513
|
-
if (!proxyResult.valid) {
|
|
514
|
-
errors.push(`Proxy: ${proxyResult.error}`);
|
|
515
|
-
}
|
|
516
|
-
// Note: Proxy URLs are marked with requiresRuntimeCheck to prevent DNS rebinding
|
|
517
|
-
// Consider implementing runtime IP validation in production environments
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Validate image timeout
|
|
521
|
-
if (options.imageTimeout !== undefined) {
|
|
522
|
-
const timeoutResult = this.validateImageTimeout(options.imageTimeout);
|
|
523
|
-
if (!timeoutResult.valid) {
|
|
524
|
-
errors.push(`Image timeout: ${timeoutResult.error}`);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// Validate dimensions
|
|
529
|
-
if (options.width !== undefined || options.height !== undefined) {
|
|
530
|
-
const width = options.width ?? 800;
|
|
531
|
-
const height = options.height ?? 600;
|
|
532
|
-
const dimensionsResult = this.validateDimensions(width, height);
|
|
533
|
-
if (!dimensionsResult.valid) {
|
|
534
|
-
errors.push(`Dimensions: ${dimensionsResult.error}`);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Validate scale
|
|
539
|
-
if (options.scale !== undefined) {
|
|
540
|
-
const scaleResult = this.validateScale(options.scale);
|
|
541
|
-
if (!scaleResult.valid) {
|
|
542
|
-
errors.push(`Scale: ${scaleResult.error}`);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Validate CSP nonce
|
|
547
|
-
if (options.cspNonce !== undefined) {
|
|
548
|
-
const nonceResult = this.validateCspNonce(options.cspNonce);
|
|
549
|
-
if (!nonceResult.valid) {
|
|
550
|
-
errors.push(`CSP nonce: ${nonceResult.error}`);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Custom validation
|
|
555
|
-
if (this.config.customValidator) {
|
|
556
|
-
const customResult = this.config.customValidator(options, 'options');
|
|
557
|
-
if (!customResult.valid) {
|
|
558
|
-
errors.push(`Custom validation: ${customResult.error}`);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (errors.length > 0) {
|
|
563
|
-
return {
|
|
564
|
-
valid: false,
|
|
565
|
-
error: errors.join('; ')
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
return { valid: true };
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Create a default validator instance
|
|
575
|
-
*/
|
|
576
|
-
export function createDefaultValidator(config: ValidatorConfig = {}): Validator {
|
|
577
|
-
return new Validator({
|
|
578
|
-
allowDataUrls: true,
|
|
579
|
-
maxImageTimeout: 300000, // 5 minutes
|
|
580
|
-
...config
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Create a strict validator with security-focused settings
|
|
586
|
-
*/
|
|
587
|
-
export function createStrictValidator(allowedProxyDomains: string[]): Validator {
|
|
588
|
-
return new Validator({
|
|
589
|
-
allowedProxyDomains,
|
|
590
|
-
allowDataUrls: false,
|
|
591
|
-
maxImageTimeout: 60000 // 1 minute
|
|
592
|
-
});
|
|
593
|
-
}
|