apx-toolkit
Version:
Automatically discover APIs and generate complete integration packages: code in 12 languages, TypeScript types, test suites, SDK packages, API documentation, mock servers, performance reports, and contract tests. Saves 2-4 weeks of work in seconds.
146 lines • 5.39 kB
JavaScript
/**
* Security utilities for APX Toolkit
* Provides path sanitization and input validation
*/
import * as path from 'path';
/**
* Sanitizes a file path to prevent directory traversal attacks
* Ensures the resolved path stays within the base directory
*
* @param userPath - User-provided path (may be relative or contain ..)
* @param baseDir - Base directory to resolve paths relative to
* @returns Sanitized absolute path
* @throws Error if path traversal is detected
*/
export function sanitizePath(userPath, baseDir) {
// Resolve base directory to absolute path
const resolvedBase = path.resolve(baseDir);
// Resolve user path relative to base directory
const resolvedPath = path.resolve(resolvedBase, userPath);
// Check if resolved path is within base directory
if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) {
throw new Error(`Path traversal detected: ${userPath} resolves outside base directory`);
}
return resolvedPath;
}
/**
* Validates URL to ensure it's safe to request
*
* @param urlString - URL to validate
* @param allowLocalhost - Whether to allow localhost URLs (default: false)
* @returns Validated URL object
* @throws Error if URL is invalid or unsafe
*/
export function validateURL(urlString, allowLocalhost = false) {
let url;
try {
url = new URL(urlString);
}
catch (error) {
throw new Error(`Invalid URL format: ${urlString}`);
}
// Enforce HTTPS for production (allow HTTP for localhost)
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
throw new Error(`Unsupported protocol: ${url.protocol}. Only http:// and https:// are allowed`);
}
// Check for localhost
const isLocalhost = url.hostname === 'localhost' ||
url.hostname === '127.0.0.1' ||
url.hostname.startsWith('192.168.') ||
url.hostname.startsWith('10.') ||
url.hostname.startsWith('172.');
// Enforce HTTPS for non-localhost (unless explicitly allowed)
if (!allowLocalhost && !isLocalhost && url.protocol !== 'https:') {
throw new Error(`HTTPS required for non-localhost URLs: ${urlString}`);
}
// Validate URL length
const MAX_URL_LENGTH = 2048;
if (urlString.length > MAX_URL_LENGTH) {
throw new Error(`URL too long: ${urlString.length} characters (max: ${MAX_URL_LENGTH})`);
}
return url;
}
/**
* Sanitizes log data to remove sensitive information
*
* @param data - Data object to sanitize
* @param sensitiveKeys - Keys to redact (default: common sensitive keys)
* @returns Sanitized data object
*/
export function sanitizeLogData(data, sensitiveKeys = ['authorization', 'x-api-key', 'cookie', 'token', 'password', 'secret', 'api-key', 'bearer']) {
if (!data || typeof data !== 'object') {
return data;
}
if (Array.isArray(data)) {
return data.map(item => sanitizeLogData(item, sensitiveKeys));
}
const sanitized = {};
for (const [key, value] of Object.entries(data)) {
const lowerKey = key.toLowerCase();
const isSensitive = sensitiveKeys.some(sk => lowerKey.includes(sk.toLowerCase()));
if (isSensitive && typeof value === 'string') {
// Redact sensitive values (show first 4 and last 4 chars)
const str = String(value);
if (str.length > 8) {
sanitized[key] = `${str.substring(0, 4)}...${str.substring(str.length - 4)}`;
}
else {
sanitized[key] = '***REDACTED***';
}
}
else if (typeof value === 'object' && value !== null) {
sanitized[key] = sanitizeLogData(value, sensitiveKeys);
}
else {
sanitized[key] = value;
}
}
return sanitized;
}
/**
* Validates input size limits
*
* @param input - Input string to validate
* @param maxSize - Maximum size in bytes
* @param fieldName - Name of the field for error messages
* @throws Error if input exceeds size limit
*/
export function validateInputSize(input, maxSize, fieldName) {
const size = Buffer.byteLength(input, 'utf8');
if (size > maxSize) {
throw new Error(`${fieldName} exceeds maximum size: ${size} bytes (max: ${maxSize} bytes)`);
}
}
/**
* Constants for input size limits
*/
export const INPUT_LIMITS = {
MAX_URL_LENGTH: 2048,
MAX_HEADER_SIZE: 8192,
MAX_BODY_SIZE: 10485760, // 10MB
MAX_FILENAME_LENGTH: 255,
};
/**
* Sanitizes a filename to prevent directory traversal and invalid characters
*
* @param filename - Filename to sanitize
* @returns Sanitized filename
*/
export function sanitizeFilename(filename) {
// Remove path separators and null bytes
let sanitized = filename.replace(/[\/\\\x00]/g, '');
// Remove leading dots and spaces (Windows issue)
sanitized = sanitized.replace(/^[.\s]+/, '');
// Limit length
if (sanitized.length > INPUT_LIMITS.MAX_FILENAME_LENGTH) {
const ext = path.extname(sanitized);
const name = path.basename(sanitized, ext);
sanitized = name.substring(0, INPUT_LIMITS.MAX_FILENAME_LENGTH - ext.length) + ext;
}
// If empty after sanitization, use a default
if (!sanitized || sanitized.trim().length === 0) {
sanitized = 'file';
}
return sanitized;
}
//# sourceMappingURL=security.js.map