@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
211 lines (210 loc) • 7.14 kB
JavaScript
;
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Security utilities for input validation and sanitization
*/
class SecurityUtilities {
// Maximum upload size: 700MB
static MAX_UPLOAD_SIZE = 700 * 1024 * 1024;
// Maximum number of files in a ZIP
static MAX_ZIP_FILES = 50000;
// Maximum decompressed size: 500MB
static MAX_DECOMPRESSED_SIZE = 500 * 1024 * 1024;
// Rate limiting: max attempts per IP
static authAttempts = new Map();
static MAX_AUTH_ATTEMPTS = 5;
static AUTH_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
/**
* Validates that a path doesn't contain directory traversal sequences
*/
static validatePath(path) {
if (!path) {
return false;
}
// Normalize the path
const normalized = path.replace(/\\/g, "/");
// Check for directory traversal patterns
if (normalized.includes("../") ||
normalized.includes("/..") ||
normalized.startsWith("..") ||
normalized.includes("/../") ||
normalized.match(/[/\\]\.\./) ||
normalized.includes("\0") // null byte
) {
return false;
}
// Check for absolute paths
if (normalized.startsWith("/") ||
normalized.match(/^[a-zA-Z]:/) // Windows drive letter
) {
return false;
}
return true;
}
/**
* Validates that a path doesn't contain directory traversal sequences.
* Unlike validatePath, this allows leading slashes for storage-relative paths.
*/
static validatePathTraversal(path) {
if (!path) {
return false;
}
// Normalize the path
const normalized = path.replace(/\\/g, "/");
// Check for directory traversal patterns
if (normalized.includes("../") ||
normalized.includes("/..") ||
normalized.startsWith("..") ||
normalized.includes("/../") ||
normalized.match(/[/\\]\.\./) ||
normalized.includes("\0") // null byte
) {
return false;
}
return true;
}
/**
* Sanitizes a storage path by removing dangerous characters while preserving leading slash.
* For use with internal storage system that expects paths like "/images/file.png".
*/
static sanitizeStoragePath(path) {
if (!path) {
return "";
}
// Remove null bytes
path = path.replace(/\0/g, "");
// Normalize slashes
path = path.replace(/\\/g, "/");
// Remove drive letters
path = path.replace(/^[a-zA-Z]:/, "");
// Split into segments and validate each (filter out . and ..)
const hasLeadingSlash = path.startsWith("/");
const segments = path.split("/").filter((segment) => {
return segment && segment !== "." && segment !== "..";
});
return (hasLeadingSlash ? "/" : "") + segments.join("/");
}
/**
* Sanitizes a path by removing dangerous characters and sequences
*/
static sanitizePath(path) {
if (!path) {
return "";
}
// Remove null bytes
path = path.replace(/\0/g, "");
// Normalize slashes
path = path.replace(/\\/g, "/");
// Remove leading slashes and drive letters
path = path.replace(/^[a-zA-Z]:/, "");
path = path.replace(/^\/+/, "");
// Split into segments and validate each
const segments = path.split("/").filter((segment) => {
return segment && segment !== "." && segment !== "..";
});
return segments.join("/");
}
/**
* Validates that a file size is within acceptable limits
*/
static validateFileSize(size, maxSize = SecurityUtilities.MAX_UPLOAD_SIZE) {
return size > 0 && size <= maxSize;
}
/**
* Validates Minecraft command input to prevent injection
*/
static sanitizeCommand(command) {
if (!command) {
return "";
}
// Remove control characters (including newlines, carriage returns, etc.)
command = command.replace(/[\x00-\x1F\x7F]/g, "");
// Trim whitespace
command = command.trim();
// Remove leading slash if present (will be added by server)
if (command.startsWith("/")) {
command = command.substring(1);
}
return command;
}
/**
* Validates that a command is safe to execute
*/
static isCommandSafe(command) {
if (!command) {
return false;
}
// Check for command separators or multiple commands
if (command.includes("\n") || command.includes("\r") || command.includes(";")) {
return false;
}
// Check for excessive length
if (command.length > 1000) {
return false;
}
return true;
}
/**
* Rate limiting for authentication attempts
*/
static checkAuthRateLimit(identifier) {
const now = Date.now();
const attempt = SecurityUtilities.authAttempts.get(identifier);
if (!attempt) {
SecurityUtilities.authAttempts.set(identifier, { count: 1, resetTime: now + SecurityUtilities.AUTH_WINDOW_MS });
return true;
}
// Reset if window has passed
if (now > attempt.resetTime) {
SecurityUtilities.authAttempts.set(identifier, { count: 1, resetTime: now + SecurityUtilities.AUTH_WINDOW_MS });
return true;
}
// Check if under limit
if (attempt.count < SecurityUtilities.MAX_AUTH_ATTEMPTS) {
attempt.count++;
return true;
}
return false;
}
/**
* Reset rate limit for an identifier (on successful auth)
*/
static resetAuthRateLimit(identifier) {
SecurityUtilities.authAttempts.delete(identifier);
}
/**
* Validates JSON object doesn't contain prototype pollution
*/
static sanitizeJsonObject(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
// Remove dangerous properties
const dangerous = ["__proto__", "constructor", "prototype"];
for (const key of dangerous) {
if (key in obj) {
delete obj[key];
}
}
// Recursively sanitize nested objects
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
obj[key] = SecurityUtilities.sanitizeJsonObject(obj[key]);
}
}
return obj;
}
/**
* Validates that a string contains only safe characters for player names
*/
static sanitizePlayerName(name) {
if (!name) {
return "";
}
// Only allow alphanumeric, spaces, underscores, and hyphens
return name.replace(/[^a-zA-Z0-9 _-]/g, "");
}
}
exports.default = SecurityUtilities;