@xapp/arachne-utils 1.10.0 → 1.10.7

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.
@@ -0,0 +1,53 @@
1
+ /*! Copyright (c) 2025, XAPP AI */
2
+ import type { Page } from "puppeteer-core";
3
+ /**
4
+ * Document type enumeration
5
+ */
6
+ export type DocumentType = "PDF" | "MS_WORD" | "MS_EXCEL" | "PPT";
7
+ /**
8
+ * Interface representing a document link detected on a page
9
+ */
10
+ export interface DocumentLink {
11
+ /**
12
+ * The absolute URL to the document
13
+ */
14
+ url: string;
15
+ /**
16
+ * The type of document
17
+ */
18
+ type: DocumentType;
19
+ /**
20
+ * Optional title/text of the link
21
+ */
22
+ title?: string;
23
+ }
24
+ /**
25
+ * All supported document extensions
26
+ */
27
+ export declare const DOCUMENT_EXTENSIONS: string[];
28
+ /**
29
+ * Detects document links on a page
30
+ *
31
+ * This function identifies document links by checking file extensions in both the pathname
32
+ * and query parameters of URLs. It detects common document formats including PDF, Word,
33
+ * Excel, and PowerPoint files.
34
+ *
35
+ * **Limitations**:
36
+ * - Only detects documents with recognizable file extensions
37
+ * - Does not detect documents served via content-type headers without extensions
38
+ * - Does not make HEAD requests to verify content types
39
+ *
40
+ * @param page - The Puppeteer page to scan for document links
41
+ * @param allowedTypes - Optional array of document types to filter by (e.g., ['PDF', 'MS_WORD'])
42
+ * @returns Promise resolving to an array of detected document links
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const documentLinks = await detectDocumentLinks(page);
47
+ * // Returns all document links
48
+ *
49
+ * const pdfLinks = await detectDocumentLinks(page, ['PDF']);
50
+ * // Returns only PDF links
51
+ * ```
52
+ */
53
+ export declare function detectDocumentLinks(page: Page, allowedTypes?: DocumentType[]): Promise<DocumentLink[]>;
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ /*! Copyright (c) 2025, XAPP AI */
3
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
5
+ return new (P || (P = Promise))(function (resolve, reject) {
6
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
7
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
8
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
9
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
10
+ });
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.DOCUMENT_EXTENSIONS = void 0;
14
+ exports.detectDocumentLinks = detectDocumentLinks;
15
+ const hasExtension_1 = require("./hasExtension");
16
+ /**
17
+ * Extension to document type mapping
18
+ */
19
+ const EXTENSION_TO_TYPE = {
20
+ '.pdf': 'PDF',
21
+ '.doc': 'MS_WORD',
22
+ '.docx': 'MS_WORD',
23
+ '.xls': 'MS_EXCEL',
24
+ '.xlsx': 'MS_EXCEL',
25
+ '.ppt': 'PPT',
26
+ '.pptx': 'PPT'
27
+ };
28
+ /**
29
+ * All supported document extensions
30
+ */
31
+ exports.DOCUMENT_EXTENSIONS = Object.keys(EXTENSION_TO_TYPE);
32
+ /**
33
+ * Extract document type from URL by checking both pathname and query parameters
34
+ * @param url - The URL to check
35
+ * @returns The document type if found, undefined otherwise
36
+ */
37
+ function getDocumentTypeFromUrl(url) {
38
+ // First check pathname for extensions
39
+ for (const [extension, type] of Object.entries(EXTENSION_TO_TYPE)) {
40
+ if ((0, hasExtension_1.hasExtension)(url, extension)) {
41
+ return type;
42
+ }
43
+ }
44
+ // Also check query parameters for extensions
45
+ // e.g., https://example.com/download?file=report.pdf&token=xyz
46
+ try {
47
+ const urlObj = new URL(url);
48
+ const queryString = urlObj.search.toLowerCase();
49
+ for (const [extension, type] of Object.entries(EXTENSION_TO_TYPE)) {
50
+ if (queryString.includes(extension)) {
51
+ return type;
52
+ }
53
+ }
54
+ }
55
+ catch (e) {
56
+ // Invalid URL, skip query parameter check
57
+ }
58
+ return undefined;
59
+ }
60
+ /**
61
+ * Detects document links on a page
62
+ *
63
+ * This function identifies document links by checking file extensions in both the pathname
64
+ * and query parameters of URLs. It detects common document formats including PDF, Word,
65
+ * Excel, and PowerPoint files.
66
+ *
67
+ * **Limitations**:
68
+ * - Only detects documents with recognizable file extensions
69
+ * - Does not detect documents served via content-type headers without extensions
70
+ * - Does not make HEAD requests to verify content types
71
+ *
72
+ * @param page - The Puppeteer page to scan for document links
73
+ * @param allowedTypes - Optional array of document types to filter by (e.g., ['PDF', 'MS_WORD'])
74
+ * @returns Promise resolving to an array of detected document links
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const documentLinks = await detectDocumentLinks(page);
79
+ * // Returns all document links
80
+ *
81
+ * const pdfLinks = await detectDocumentLinks(page, ['PDF']);
82
+ * // Returns only PDF links
83
+ * ```
84
+ */
85
+ function detectDocumentLinks(page, allowedTypes) {
86
+ return __awaiter(this, void 0, void 0, function* () {
87
+ const baseUrl = page.url();
88
+ // Extract all links from the page
89
+ const links = yield page.$$eval('a[href]', (anchors, baseUrl) => {
90
+ return anchors.map(anchor => {
91
+ var _a;
92
+ const href = anchor.getAttribute('href');
93
+ if (!href)
94
+ return null;
95
+ try {
96
+ // Resolve relative URLs
97
+ const absoluteUrl = new URL(href, baseUrl).toString();
98
+ const text = ((_a = anchor.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || anchor.getAttribute('title') || '';
99
+ return {
100
+ url: absoluteUrl,
101
+ title: text
102
+ };
103
+ }
104
+ catch (e) {
105
+ // Invalid URL, skip it
106
+ return null;
107
+ }
108
+ }).filter(link => link !== null);
109
+ }, baseUrl);
110
+ // Filter and classify document links
111
+ const documentLinks = [];
112
+ for (const link of links) {
113
+ if (!link)
114
+ continue;
115
+ // Get document type from URL (checks both pathname and query parameters)
116
+ const type = getDocumentTypeFromUrl(link.url);
117
+ if (type) {
118
+ // If allowedTypes is specified, filter by it
119
+ if (!allowedTypes || allowedTypes.includes(type)) {
120
+ documentLinks.push({
121
+ url: link.url,
122
+ type,
123
+ title: link.title
124
+ });
125
+ }
126
+ }
127
+ }
128
+ return documentLinks;
129
+ });
130
+ }
131
+ //# sourceMappingURL=detectDocumentLinks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detectDocumentLinks.js","sourceRoot":"","sources":["../src/detectDocumentLinks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ /*! Copyright (c) 2025, XAPP AI */
2
+ /**
3
+ * Default maximum file size for downloads (50MB)
4
+ */
5
+ export declare const DEFAULT_MAX_FILE_SIZE: number;
6
+ /**
7
+ * Default download timeout (30 seconds)
8
+ */
9
+ export declare const DEFAULT_DOWNLOAD_TIMEOUT = 30000;
10
+ /**
11
+ * Options for downloading documents
12
+ */
13
+ export interface DownloadDocumentOptions {
14
+ /**
15
+ * Maximum file size in bytes. Downloads larger than this will be rejected.
16
+ * Default: 50MB (52,428,800 bytes)
17
+ */
18
+ maxFileSize?: number;
19
+ /**
20
+ * Download timeout in milliseconds
21
+ * Default: 30000 (30 seconds)
22
+ */
23
+ timeout?: number;
24
+ /**
25
+ * Whether to perform SSRF protection checks
26
+ * Default: true
27
+ */
28
+ ssrfProtection?: boolean;
29
+ /**
30
+ * Optional custom headers to include in the request
31
+ */
32
+ headers?: Record<string, string>;
33
+ /**
34
+ * Whether to validate Content-Type header matches expected document types
35
+ * Default: true
36
+ */
37
+ validateContentType?: boolean;
38
+ }
39
+ /**
40
+ * Result of a document download operation
41
+ */
42
+ export interface DownloadDocumentResult {
43
+ /**
44
+ * The downloaded document as a Buffer
45
+ */
46
+ data: Buffer;
47
+ /**
48
+ * Content-Type header from the response
49
+ */
50
+ contentType?: string;
51
+ /**
52
+ * Content-Length header from the response (file size in bytes)
53
+ */
54
+ contentLength?: number;
55
+ /**
56
+ * The final URL after any redirects
57
+ */
58
+ finalUrl: string;
59
+ }
60
+ /**
61
+ * Download a document from a URL with safety checks and size limits
62
+ *
63
+ * This function provides:
64
+ * - SSRF protection (blocks private IPs, localhost, metadata endpoints)
65
+ * - File size limits to prevent memory exhaustion
66
+ * - Streaming download with incremental size checking
67
+ * - Timeout handling
68
+ * - Content-Type validation
69
+ *
70
+ * The download uses streaming to protect against memory exhaustion attacks. If a server
71
+ * sends more data than the `maxFileSize` limit, the download is aborted immediately
72
+ * rather than loading the entire file into memory.
73
+ *
74
+ * @param url - The URL of the document to download
75
+ * @param options - Download options
76
+ * @returns Promise resolving to the downloaded document data
77
+ * @throws Error if download fails, exceeds size limit, or violates SSRF protection
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * try {
82
+ * const result = await downloadDocument('https://example.com/doc.pdf', {
83
+ * maxFileSize: 10 * 1024 * 1024, // 10MB
84
+ * timeout: 15000 // 15 seconds
85
+ * });
86
+ *
87
+ * // Save to file
88
+ * fs.writeFileSync('downloaded.pdf', result.data);
89
+ * } catch (error) {
90
+ * console.error('Download failed:', error.message);
91
+ * }
92
+ * ```
93
+ */
94
+ export declare function downloadDocument(url: string, options?: DownloadDocumentOptions): Promise<DownloadDocumentResult>;
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ /*! Copyright (c) 2025, XAPP AI */
3
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
5
+ return new (P || (P = Promise))(function (resolve, reject) {
6
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
7
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
8
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
9
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
10
+ });
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.DEFAULT_DOWNLOAD_TIMEOUT = exports.DEFAULT_MAX_FILE_SIZE = void 0;
14
+ exports.downloadDocument = downloadDocument;
15
+ const ssrfProtection_1 = require("./ssrfProtection");
16
+ /**
17
+ * Default maximum file size for downloads (50MB)
18
+ */
19
+ exports.DEFAULT_MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB in bytes
20
+ /**
21
+ * Default download timeout (30 seconds)
22
+ */
23
+ exports.DEFAULT_DOWNLOAD_TIMEOUT = 30000; // 30 seconds
24
+ /**
25
+ * Expected Content-Type values for document types
26
+ */
27
+ const DOCUMENT_CONTENT_TYPES = [
28
+ 'application/pdf',
29
+ 'application/msword',
30
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
31
+ 'application/vnd.ms-excel',
32
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
33
+ 'application/vnd.ms-powerpoint',
34
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
35
+ 'application/octet-stream', // Sometimes servers use generic binary type
36
+ ];
37
+ /**
38
+ * Download a document from a URL with safety checks and size limits
39
+ *
40
+ * This function provides:
41
+ * - SSRF protection (blocks private IPs, localhost, metadata endpoints)
42
+ * - File size limits to prevent memory exhaustion
43
+ * - Streaming download with incremental size checking
44
+ * - Timeout handling
45
+ * - Content-Type validation
46
+ *
47
+ * The download uses streaming to protect against memory exhaustion attacks. If a server
48
+ * sends more data than the `maxFileSize` limit, the download is aborted immediately
49
+ * rather than loading the entire file into memory.
50
+ *
51
+ * @param url - The URL of the document to download
52
+ * @param options - Download options
53
+ * @returns Promise resolving to the downloaded document data
54
+ * @throws Error if download fails, exceeds size limit, or violates SSRF protection
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * try {
59
+ * const result = await downloadDocument('https://example.com/doc.pdf', {
60
+ * maxFileSize: 10 * 1024 * 1024, // 10MB
61
+ * timeout: 15000 // 15 seconds
62
+ * });
63
+ *
64
+ * // Save to file
65
+ * fs.writeFileSync('downloaded.pdf', result.data);
66
+ * } catch (error) {
67
+ * console.error('Download failed:', error.message);
68
+ * }
69
+ * ```
70
+ */
71
+ function downloadDocument(url_1) {
72
+ return __awaiter(this, arguments, void 0, function* (url, options = {}) {
73
+ var _a;
74
+ const { maxFileSize = exports.DEFAULT_MAX_FILE_SIZE, timeout = exports.DEFAULT_DOWNLOAD_TIMEOUT, ssrfProtection = true, headers = {}, validateContentType = true } = options;
75
+ // SSRF protection check
76
+ if (ssrfProtection && !(0, ssrfProtection_1.isSafeURL)(url)) {
77
+ throw new Error(`SSRF protection: URL is not safe to fetch: ${url}`);
78
+ }
79
+ // Create AbortController for timeout
80
+ const controller = new AbortController();
81
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
82
+ try {
83
+ const response = yield fetch(url, {
84
+ headers: Object.assign({ 'User-Agent': 'Mozilla/5.0 (compatible; Arachne/1.0; +https://github.com/xapp-ai/arachne)' }, headers),
85
+ signal: controller.signal,
86
+ redirect: 'follow'
87
+ });
88
+ if (!response.ok) {
89
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
90
+ }
91
+ // Validate Content-Type if requested
92
+ const contentType = response.headers.get('content-type');
93
+ if (validateContentType && contentType) {
94
+ const isValidType = DOCUMENT_CONTENT_TYPES.some(type => contentType.toLowerCase().includes(type.toLowerCase()));
95
+ if (!isValidType) {
96
+ throw new Error(`Invalid Content-Type: ${contentType}. Expected a document type.`);
97
+ }
98
+ }
99
+ // Check Content-Length before downloading
100
+ const contentLengthHeader = response.headers.get('content-length');
101
+ if (contentLengthHeader) {
102
+ const contentLength = parseInt(contentLengthHeader, 10);
103
+ if (contentLength > maxFileSize) {
104
+ throw new Error(`File size (${contentLength} bytes) exceeds maximum allowed size (${maxFileSize} bytes)`);
105
+ }
106
+ }
107
+ // Stream the download and check size incrementally to prevent memory exhaustion
108
+ const reader = (_a = response.body) === null || _a === void 0 ? void 0 : _a.getReader();
109
+ if (!reader) {
110
+ throw new Error('Response body is not readable');
111
+ }
112
+ const chunks = [];
113
+ let receivedLength = 0;
114
+ try {
115
+ while (true) {
116
+ const { done, value } = yield reader.read();
117
+ if (done)
118
+ break;
119
+ receivedLength += value.length;
120
+ // Check size during download to prevent memory exhaustion
121
+ if (receivedLength > maxFileSize) {
122
+ reader.cancel();
123
+ throw new Error(`File size exceeds maximum allowed size (${maxFileSize} bytes) during download`);
124
+ }
125
+ chunks.push(value);
126
+ }
127
+ }
128
+ catch (error) {
129
+ // Ensure reader is cancelled on any error
130
+ try {
131
+ yield reader.cancel();
132
+ }
133
+ catch (_b) {
134
+ // Ignore cancellation errors
135
+ }
136
+ throw error;
137
+ }
138
+ // Combine chunks into a single buffer
139
+ const buffer = Buffer.concat(chunks.map(chunk => Buffer.from(chunk)));
140
+ return {
141
+ data: buffer,
142
+ contentType: contentType || undefined,
143
+ contentLength: buffer.length,
144
+ finalUrl: response.url
145
+ };
146
+ }
147
+ catch (error) {
148
+ if (error.name === 'AbortError') {
149
+ throw new Error(`Download timeout after ${timeout}ms`);
150
+ }
151
+ throw error;
152
+ }
153
+ finally {
154
+ clearTimeout(timeoutId);
155
+ }
156
+ });
157
+ }
158
+ //# sourceMappingURL=downloadDocument.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"downloadDocument.js","sourceRoot":"","sources":["../src/downloadDocument.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /*! Copyright (c) 2025, XAPP AI */
2
+ /**
3
+ * Check if an IP address (v4 or v6) is private or reserved
4
+ *
5
+ * This includes:
6
+ * - Loopback addresses (127.0.0.1, ::1)
7
+ * - Private network ranges (10.x.x.x, 192.168.x.x, 172.16-31.x.x)
8
+ * - Link-local addresses (169.254.x.x, fe80::)
9
+ * - Other reserved ranges
10
+ *
11
+ * @param ip - The IP address to check
12
+ * @returns true if the IP is private/reserved, false otherwise
13
+ */
14
+ export declare function isPrivateIP(ip: string): boolean;
15
+ /**
16
+ * Check if a URL is safe to fetch (not vulnerable to SSRF attacks)
17
+ *
18
+ * This function blocks:
19
+ * - localhost and 127.0.0.1
20
+ * - Private IP ranges (10.x.x.x, 192.168.x.x, 172.16-31.x.x)
21
+ * - AWS metadata endpoint (169.254.169.254)
22
+ * - IPv6 local addresses (::1, fe80::, etc.)
23
+ * - Cloud provider metadata endpoints
24
+ *
25
+ * **Security Limitation**: This function only checks the URL at the time of the call.
26
+ * It does not protect against DNS rebinding attacks, where a malicious domain could
27
+ * resolve to a public IP during this check but then resolve to a private IP (e.g., 127.0.0.1)
28
+ * during the actual fetch. For high-security environments, consider implementing additional
29
+ * DNS resolution validation or using a network-level SSRF protection solution.
30
+ *
31
+ * @param url - The URL to validate
32
+ * @returns true if the URL is safe to fetch, false if it should be blocked
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * if (!isSafeURL('http://localhost/file.pdf')) {
37
+ * throw new Error('SSRF protection: Cannot fetch from localhost');
38
+ * }
39
+ * ```
40
+ */
41
+ export declare function isSafeURL(url: string | URL): boolean;
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ /*! Copyright (c) 2025, XAPP AI */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.isPrivateIP = isPrivateIP;
5
+ exports.isSafeURL = isSafeURL;
6
+ /**
7
+ * Private IPv4 ranges as defined by RFC 1918 and other reserved ranges
8
+ */
9
+ const PRIVATE_IPV4_RANGES = [
10
+ { min: '10.0.0.0', max: '10.255.255.255' }, // Class A private
11
+ { min: '172.16.0.0', max: '172.31.255.255' }, // Class B private
12
+ { min: '192.168.0.0', max: '192.168.255.255' }, // Class C private
13
+ { min: '127.0.0.0', max: '127.255.255.255' }, // Loopback
14
+ { min: '169.254.0.0', max: '169.254.255.255' }, // Link-local (AWS metadata)
15
+ { min: '0.0.0.0', max: '0.255.255.255' }, // Current network
16
+ ];
17
+ /**
18
+ * Private/reserved IPv6 ranges
19
+ */
20
+ const PRIVATE_IPV6_PATTERNS = [
21
+ /^::1$/, // Loopback
22
+ /^::$/, // Unspecified
23
+ /^fe80:/, // Link-local
24
+ /^fc00:/, // Unique local
25
+ /^fd00:/, // Unique local
26
+ ];
27
+ /**
28
+ * Blocked hostnames that should never be accessible
29
+ */
30
+ const BLOCKED_HOSTNAMES = [
31
+ 'localhost',
32
+ 'metadata.google.internal', // GCP metadata
33
+ ];
34
+ /**
35
+ * Convert IPv4 address string to a number for comparison
36
+ * Returns -1 if the IP is invalid
37
+ */
38
+ function ipv4ToNumber(ip) {
39
+ const parts = ip.split('.').map(Number);
40
+ // Validate that we have exactly 4 parts and each is in valid range (0-255)
41
+ if (parts.length !== 4 || parts.some(p => p < 0 || p > 255 || isNaN(p))) {
42
+ return -1;
43
+ }
44
+ return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
45
+ }
46
+ /**
47
+ * Check if an IPv4 address is in a private range
48
+ */
49
+ function isPrivateIPv4(ip) {
50
+ const ipNum = ipv4ToNumber(ip);
51
+ // Invalid IP address - treat as unsafe
52
+ if (ipNum === -1) {
53
+ return true;
54
+ }
55
+ for (const range of PRIVATE_IPV4_RANGES) {
56
+ const minNum = ipv4ToNumber(range.min);
57
+ const maxNum = ipv4ToNumber(range.max);
58
+ if (ipNum >= minNum && ipNum <= maxNum) {
59
+ return true;
60
+ }
61
+ }
62
+ return false;
63
+ }
64
+ /**
65
+ * Check if an IPv6 address is private/reserved
66
+ */
67
+ function isPrivateIPv6(ip) {
68
+ const normalized = ip.toLowerCase();
69
+ for (const pattern of PRIVATE_IPV6_PATTERNS) {
70
+ if (pattern.test(normalized)) {
71
+ return true;
72
+ }
73
+ }
74
+ return false;
75
+ }
76
+ /**
77
+ * Check if an IP address (v4 or v6) is private or reserved
78
+ *
79
+ * This includes:
80
+ * - Loopback addresses (127.0.0.1, ::1)
81
+ * - Private network ranges (10.x.x.x, 192.168.x.x, 172.16-31.x.x)
82
+ * - Link-local addresses (169.254.x.x, fe80::)
83
+ * - Other reserved ranges
84
+ *
85
+ * @param ip - The IP address to check
86
+ * @returns true if the IP is private/reserved, false otherwise
87
+ */
88
+ function isPrivateIP(ip) {
89
+ // Check for IPv4
90
+ if (/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) {
91
+ return isPrivateIPv4(ip);
92
+ }
93
+ // Check for IPv6
94
+ if (ip.includes(':')) {
95
+ return isPrivateIPv6(ip);
96
+ }
97
+ return false;
98
+ }
99
+ /**
100
+ * Check if a URL is safe to fetch (not vulnerable to SSRF attacks)
101
+ *
102
+ * This function blocks:
103
+ * - localhost and 127.0.0.1
104
+ * - Private IP ranges (10.x.x.x, 192.168.x.x, 172.16-31.x.x)
105
+ * - AWS metadata endpoint (169.254.169.254)
106
+ * - IPv6 local addresses (::1, fe80::, etc.)
107
+ * - Cloud provider metadata endpoints
108
+ *
109
+ * **Security Limitation**: This function only checks the URL at the time of the call.
110
+ * It does not protect against DNS rebinding attacks, where a malicious domain could
111
+ * resolve to a public IP during this check but then resolve to a private IP (e.g., 127.0.0.1)
112
+ * during the actual fetch. For high-security environments, consider implementing additional
113
+ * DNS resolution validation or using a network-level SSRF protection solution.
114
+ *
115
+ * @param url - The URL to validate
116
+ * @returns true if the URL is safe to fetch, false if it should be blocked
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * if (!isSafeURL('http://localhost/file.pdf')) {
121
+ * throw new Error('SSRF protection: Cannot fetch from localhost');
122
+ * }
123
+ * ```
124
+ */
125
+ function isSafeURL(url) {
126
+ let parsedUrl;
127
+ try {
128
+ parsedUrl = typeof url === 'string' ? new URL(url) : url;
129
+ }
130
+ catch (e) {
131
+ // Invalid URL
132
+ return false;
133
+ }
134
+ // Only allow HTTP and HTTPS protocols
135
+ if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
136
+ return false;
137
+ }
138
+ const hostname = parsedUrl.hostname.toLowerCase();
139
+ // Check blocked hostnames
140
+ if (BLOCKED_HOSTNAMES.includes(hostname)) {
141
+ return false;
142
+ }
143
+ // Check if hostname is an IP address (v4 or v6)
144
+ // For IPv4
145
+ if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) {
146
+ return !isPrivateIPv4(hostname);
147
+ }
148
+ // For IPv6 (can be wrapped in brackets in hostname)
149
+ const ipv6Match = hostname.match(/^\[?([a-f0-9:]+)\]?$/i);
150
+ if (ipv6Match) {
151
+ return !isPrivateIPv6(ipv6Match[1]);
152
+ }
153
+ // Hostname is a domain name, which is generally safe
154
+ // (DNS rebinding attacks are a separate concern)
155
+ return true;
156
+ }
157
+ //# sourceMappingURL=ssrfProtection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssrfProtection.js","sourceRoot":"","sources":["../src/ssrfProtection.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xapp/arachne-utils",
3
- "version": "1.10.0",
3
+ "version": "1.10.7",
4
4
  "types": "lib/index",
5
5
  "main": "lib/index",
6
6
  "files": [
@@ -14,10 +14,10 @@
14
14
  "node": "^14 || ^16 || ^18 || ^20 || ^22"
15
15
  },
16
16
  "devDependencies": {
17
- "@microsoft/api-extractor": "7.55.2",
17
+ "@microsoft/api-extractor": "7.57.2",
18
18
  "@types/chai": "4.3.20",
19
19
  "@types/mocha": "10.0.10",
20
- "@types/node": "22.18.1",
20
+ "@types/node": "22.19.7",
21
21
  "@types/sinon": "17.0.4",
22
22
  "@types/sinon-chai": "3.2.12",
23
23
  "@xapp/config": "0.2.3",
@@ -38,5 +38,5 @@
38
38
  "clean": "rm -rf ./lib/*",
39
39
  "test": "mocha --recursive -r ts-node/register \"./src/**/*.test.ts\""
40
40
  },
41
- "gitHead": "155ba31908c244d545a00ccd40064d798ebf8e3e"
41
+ "gitHead": "eafca4e497b35839d1c92dd25ccbd38c0d57e3b6"
42
42
  }