@xapp/arachne-utils 1.10.1 → 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.
- package/lib/detectDocumentLinks.d.ts +53 -0
- package/lib/detectDocumentLinks.js +131 -0
- package/lib/detectDocumentLinks.js.map +1 -0
- package/lib/downloadDocument.d.ts +94 -0
- package/lib/downloadDocument.js +158 -0
- package/lib/downloadDocument.js.map +1 -0
- package/lib/ssrfProtection.d.ts +41 -0
- package/lib/ssrfProtection.js +157 -0
- package/lib/ssrfProtection.js.map +1 -0
- package/package.json +3 -3
|
@@ -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.
|
|
3
|
+
"version": "1.10.7",
|
|
4
4
|
"types": "lib/index",
|
|
5
5
|
"main": "lib/index",
|
|
6
6
|
"files": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"node": "^14 || ^16 || ^18 || ^20 || ^22"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@microsoft/api-extractor": "7.
|
|
17
|
+
"@microsoft/api-extractor": "7.57.2",
|
|
18
18
|
"@types/chai": "4.3.20",
|
|
19
19
|
"@types/mocha": "10.0.10",
|
|
20
20
|
"@types/node": "22.19.7",
|
|
@@ -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": "
|
|
41
|
+
"gitHead": "eafca4e497b35839d1c92dd25ccbd38c0d57e3b6"
|
|
42
42
|
}
|