claude-git-hooks
Version:
Git hooks with Claude CLI for code analysis and automatic commit messages
181 lines (156 loc) • 5.07 kB
JavaScript
/**
* File: sanitize.js
* Purpose: Input sanitization utilities for security
*
* Why: Prevent prompt injection and ensure valid API input
* Used by: github-client.js, and any module handling external input
*/
import logger from './logger.js';
/**
* Sanitize string input for use in prompts and API calls
* Why: Prevent prompt injection and ensure valid GitHub API input
*
* @param {string} input - Raw input string
* @param {Object} options - Sanitization options
* @param {number} options.maxLength - Maximum allowed length (default: 65536)
* @param {boolean} options.allowNewlines - Allow newline characters (default: true)
* @param {boolean} options.stripControlChars - Remove control characters (default: true)
* @returns {string} - Sanitized string
*/
export const sanitizeInput = (input, {
maxLength = 65536,
allowNewlines = true,
stripControlChars = true
} = {}) => {
if (typeof input !== 'string') {
return '';
}
let sanitized = input;
// Strip control characters (except newlines/tabs if allowed)
if (stripControlChars) {
if (allowNewlines) {
// Keep \n, \r, \t but remove other control chars
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
} else {
// Remove all control characters including newlines
sanitized = sanitized.replace(/[\x00-\x1F\x7F]/g, ' ');
}
}
// Truncate to max length
if (sanitized.length > maxLength) {
sanitized = sanitized.substring(0, maxLength);
logger.debug('sanitize - sanitizeInput', 'Input truncated', {
originalLength: input.length,
maxLength
});
}
return sanitized.trim();
};
/**
* Sanitize PR title for GitHub API
* Why: Titles have specific requirements (single line, reasonable length)
*
* @param {string} title - Raw title
* @returns {string} - Sanitized title
*/
export const sanitizePRTitle = (title) => {
return sanitizeInput(title, {
maxLength: 256,
allowNewlines: false,
stripControlChars: true
});
};
/**
* Sanitize PR body/description for GitHub API
* Why: Body can be longer but still needs control char removal
*
* @param {string} body - Raw body text
* @returns {string} - Sanitized body
*/
export const sanitizePRBody = (body) => {
return sanitizeInput(body, {
maxLength: 65536, // GitHub's limit is 65536 chars
allowNewlines: true,
stripControlChars: true
});
};
/**
* Sanitize array of strings (labels, reviewers)
* Why: Ensure valid GitHub usernames/label names
*
* @param {Array<string>} items - Array of strings to sanitize
* @param {RegExp} validPattern - Pattern for valid items (default: alphanumeric + hyphen + dot)
* @returns {Array<string>} - Sanitized array
*/
export const sanitizeStringArray = (items, validPattern = /^[\w.-]+$/) => {
if (!Array.isArray(items)) {
return [];
}
return items
.filter(item => typeof item === 'string')
.map(item => item.trim())
.filter(item => item.length > 0 && item.length <= 100)
.filter(item => validPattern.test(item));
};
/**
* Sanitize GitHub username
* Why: Usernames have specific format requirements
*
* @param {string} username - Raw username
* @returns {string|null} - Sanitized username or null if invalid
*/
export const sanitizeUsername = (username) => {
if (typeof username !== 'string') {
return null;
}
const trimmed = username.trim().replace(/^@/, ''); // Remove leading @
// GitHub username rules: alphanumeric + hyphen, 1-39 chars, no consecutive hyphens
if (!/^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(trimmed)) {
return null;
}
if (trimmed.length > 39 || trimmed.includes('--')) {
return null;
}
return trimmed;
};
/**
* Sanitize GitHub label name
* Why: Labels have format requirements
*
* @param {string} label - Raw label name
* @returns {string|null} - Sanitized label or null if invalid
*/
export const sanitizeLabel = (label) => {
if (typeof label !== 'string') {
return null;
}
const trimmed = label.trim();
// Labels can contain most chars except commas, max 50 chars
if (trimmed.length === 0 || trimmed.length > 50) {
return null;
}
// Remove problematic characters
return trimmed.replace(/[,\x00-\x1F\x7F]/g, '');
};
/**
* Sanitize branch name
* Why: Branch names have git format requirements
*
* @param {string} branch - Raw branch name
* @returns {string|null} - Sanitized branch or null if invalid
*/
export const sanitizeBranchName = (branch) => {
if (typeof branch !== 'string') {
return null;
}
const trimmed = branch.trim();
// Git branch name rules (simplified)
if (!/^[\w./-]+$/.test(trimmed)) {
return null;
}
// No consecutive dots, no ending with .lock
if (trimmed.includes('..') || trimmed.endsWith('.lock')) {
return null;
}
return trimmed;
};