@dharshansr/gitgenius
Version:
AI-powered commit message generator with enhanced features
227 lines • 7.74 kB
JavaScript
import crypto from 'crypto';
import { URL } from 'url';
/**
* Security utilities for input validation, sanitization, and encryption
*/
export class SecurityUtils {
/**
* Validate and enforce HTTPS URLs
*/
static validateSecureUrl(urlString) {
try {
const url = new URL(urlString);
return url.protocol === 'https:';
}
catch {
return false;
}
}
/**
* Enforce HTTPS for API endpoints
*/
static enforceHttps(urlString) {
const url = new URL(urlString);
if (url.protocol !== 'https:') {
throw new Error(`Insecure protocol detected. HTTPS is required for all API communications. URL: ${url.protocol}//${url.host}`);
}
return urlString;
}
/**
* Sanitize input to prevent injection attacks
*/
static sanitizeInput(input) {
if (typeof input !== 'string') {
throw new Error('Input must be a string');
}
// Remove null bytes
let sanitized = input.replace(/\0/g, '');
// Remove control characters except newlines and tabs
// eslint-disable-next-line no-control-regex
sanitized = sanitized.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, '');
// Limit length to prevent DoS
const MAX_LENGTH = 100000; // 100KB
if (sanitized.length > MAX_LENGTH) {
throw new Error(`Input exceeds maximum allowed length of ${MAX_LENGTH} characters`);
}
return sanitized;
}
/**
* Validate API key format
*/
static validateApiKey(apiKey) {
if (!apiKey || typeof apiKey !== 'string') {
return false;
}
// Check minimum length
if (apiKey.length < 20) {
return false;
}
// Check for printable ASCII characters only
const printableAscii = /^[\x20-\x7E]+$/;
return printableAscii.test(apiKey);
}
/**
* Encrypt sensitive data
*/
static encrypt(text, masterPassword) {
// Generate a key from the master password
const salt = crypto.randomBytes(SecurityUtils.SALT_LENGTH);
const key = crypto.pbkdf2Sync(masterPassword, salt, 100000, SecurityUtils.KEY_LENGTH, 'sha256');
// Generate IV
const iv = crypto.randomBytes(SecurityUtils.IV_LENGTH);
// Encrypt
const cipher = crypto.createCipheriv(SecurityUtils.ALGORITHM, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Get auth tag
const tag = cipher.getAuthTag();
// Combine salt + iv + tag + encrypted
return salt.toString('hex') +
':' + iv.toString('hex') +
':' + tag.toString('hex') +
':' + encrypted;
}
/**
* Decrypt sensitive data
*/
static decrypt(encryptedData, masterPassword) {
const parts = encryptedData.split(':');
if (parts.length !== 4) {
throw new Error('Invalid encrypted data format');
}
const salt = Buffer.from(parts[0], 'hex');
const iv = Buffer.from(parts[1], 'hex');
const tag = Buffer.from(parts[2], 'hex');
const encrypted = parts[3];
// Derive key from password
const key = crypto.pbkdf2Sync(masterPassword, salt, 100000, SecurityUtils.KEY_LENGTH, 'sha256');
// Decrypt
const decipher = crypto.createDecipheriv(SecurityUtils.ALGORITHM, key, iv);
decipher.setAuthTag(tag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
/**
* Generate a secure random token for API key rotation
*/
static generateRotationToken() {
return crypto.randomBytes(32).toString('hex');
}
/**
* Hash sensitive data for comparison (one-way)
*/
static hash(data) {
return crypto.createHash('sha256').update(data).digest('hex');
}
/**
* Validate commit message to prevent injection
*/
static validateCommitMessage(message) {
if (!message || typeof message !== 'string') {
return false;
}
// Check length
if (message.length === 0 || message.length > 10000) {
return false;
}
// Check for suspicious patterns that might indicate injection attempts
const suspiciousPatterns = [
/\$\{.*\}/, // Template injection
/<script/i, // XSS attempts
/javascript:/i, // JavaScript protocol
/on\w+\s*=/i, // Event handlers
/eval\s*\(/i, // eval() calls
];
return !suspiciousPatterns.some(pattern => pattern.test(message));
}
/**
* Check rate limit for a given key
*/
static checkRateLimit(key, maxRequests, windowMs) {
const now = Date.now();
const record = SecurityUtils.requestCounts.get(key);
if (!record || now > record.resetTime) {
// New window
SecurityUtils.requestCounts.set(key, {
count: 1,
resetTime: now + windowMs
});
return true;
}
if (record.count >= maxRequests) {
return false;
}
record.count++;
return true;
}
/**
* Clean up old rate limit records
*/
static cleanupRateLimits() {
const now = Date.now();
for (const [key, record] of SecurityUtils.requestCounts.entries()) {
if (now > record.resetTime) {
SecurityUtils.requestCounts.delete(key);
}
}
}
/**
* Generate secure headers for API requests
*/
static getSecureHeaders(apiKey) {
const headers = {
'Content-Type': 'application/json',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'User-Agent': 'GitGenius/1.2.0'
};
if (apiKey) {
headers['Authorization'] = `Bearer ${apiKey}`;
}
return headers;
}
/**
* Validate environment variable name to prevent injection
*/
static validateEnvVarName(name) {
// Only allow alphanumeric and underscore
return /^[A-Z_][A-Z0-9_]*$/.test(name);
}
/**
* Sanitize file path to prevent directory traversal
*/
static sanitizePath(path) {
// Remove any attempts at directory traversal
// eslint-disable-next-line no-useless-escape
const sanitized = path.replace(/\.\./g, '').replace(/[\/\\]+/g, '/');
// Check for suspicious patterns
if (sanitized.includes('~') || sanitized.startsWith('/')) {
throw new Error('Invalid path: absolute paths and home directory references are not allowed');
}
return sanitized;
}
/**
* Mask sensitive data for logging
*/
static maskSensitiveData(data, visibleChars = 4) {
if (!data || data.length <= visibleChars * 2) {
return '***';
}
const start = data.substring(0, visibleChars);
const end = data.substring(data.length - visibleChars);
return `${start}${'*'.repeat(data.length - visibleChars * 2)}${end}`;
}
}
SecurityUtils.ALGORITHM = 'aes-256-gcm';
SecurityUtils.SALT_LENGTH = 16;
SecurityUtils.IV_LENGTH = 16;
SecurityUtils.TAG_LENGTH = 16;
SecurityUtils.KEY_LENGTH = 32;
/**
* Rate limiting helper - track requests
*/
SecurityUtils.requestCounts = new Map();
//# sourceMappingURL=SecurityUtils.js.map