@himorishige/noren-plugin-security
Version:
Security-focused plugin for Noren (JWT tokens, API keys, HTTP headers, cookies)
132 lines (131 loc) • 4.69 kB
JavaScript
// Optional debug logging function
let debugLogger;
export function setSecurityDebugLogger(logger) {
debugLogger = logger;
}
export function logSecurityError(context, error, input) {
if (debugLogger) {
const sanitizedInput = input
? `Input: "${input.slice(0, 50)}${input.length > 50 ? '...' : ''}"`
: '';
debugLogger(`Security parsing error in ${context}. ${sanitizedInput}`, error);
}
}
/**
* Generic header/cookie parser
*/
function parseNameValuePairs(input, separator = ';') {
const pairs = [];
const items = input.split(separator);
for (const item of items) {
const trimmed = item.trim();
const equalIndex = trimmed.indexOf('=');
if (equalIndex > 0) {
const name = trimmed.substring(0, equalIndex).trim();
const value = trimmed.substring(equalIndex + 1).trim();
pairs.push({ name, value });
}
}
return pairs;
}
/** Parse Cookie header string */
export function parseCookieHeader(cookieHeader) {
const cookieValue = cookieHeader.replace(/^Cookie\s*:\s*/i, '');
return parseNameValuePairs(cookieValue).map(({ name, value }) => ({
name,
value,
isAllowed: false, // Updated later by allowlist check
}));
}
/** Parse Set-Cookie header string */
export function parseSetCookieHeader(setCookieHeader) {
const cookieValue = setCookieHeader.replace(/^Set-Cookie\s*:\s*/i, '');
const pairs = parseNameValuePairs(cookieValue);
if (pairs.length === 0)
return null;
const { name, value } = pairs[0];
return {
name,
value,
isAllowed: false, // Updated later by allowlist check
};
}
/** Parse HTTP header string */
export function parseHttpHeader(headerLine) {
const colonIndex = headerLine.indexOf(':');
if (colonIndex <= 0)
return null;
const name = headerLine.substring(0, colonIndex).trim();
const value = headerLine.substring(colonIndex + 1).trim();
return {
name,
value,
isAllowed: false, // Updated later by allowlist check
};
}
/**
* Generic allowlist checker for names with wildcard support
*/
function isInAllowlist(itemName, allowlist) {
if (!allowlist || allowlist.length === 0)
return false;
const lowerItem = itemName.toLowerCase();
return allowlist.some((allowed) => {
const lowerAllowed = allowed.toLowerCase();
// Exact match
if (lowerAllowed === lowerItem)
return true;
// Wildcard pattern (ending with *)
if (allowed.endsWith('*')) {
const prefix = lowerAllowed.slice(0, -1);
return lowerItem.startsWith(prefix);
}
return false;
});
}
/** Check if cookie is in allowlist */
export function isCookieAllowed(cookieName, config) {
return isInAllowlist(cookieName, config?.cookieAllowlist);
}
/** Check if header is in allowlist */
export function isHeaderAllowed(headerName, config) {
return isInAllowlist(headerName, config?.headerAllowlist);
}
/** Apply default values to security configuration */
export function applyDefaultConfig(config = {}) {
return {
cookieAllowlist: config.cookieAllowlist ?? [],
headerAllowlist: config.headerAllowlist ?? [],
strictMode: config.strictMode ?? false,
jwtMinLength: config.jwtMinLength ?? 50,
apiKeyMinLength: config.apiKeyMinLength ?? 16,
};
}
/**
* Validate allowlist patterns for security
*/
function validateAllowlistPatterns(patterns, type) {
for (const pattern of patterns) {
if (typeof pattern !== 'string') {
throw new Error(`Invalid ${type} allowlist pattern: must be string, got ${typeof pattern}`);
}
// Check for dangerous characters
if (pattern.includes('\0') || pattern.includes('\n') || pattern.includes('\r')) {
throw new Error(`Invalid ${type} allowlist pattern: contains dangerous characters: ${pattern}`);
}
// Validate wildcard usage (only * at end is allowed)
const asteriskCount = (pattern.match(/\*/g) || []).length;
if (asteriskCount > 1 || (asteriskCount === 1 && !pattern.endsWith('*'))) {
throw new Error(`Invalid ${type} allowlist pattern: wildcard (*) only allowed at end: ${pattern}`);
}
}
}
/** Validate security configuration to prevent injection attacks */
export function validateSecurityConfig(config) {
if (config.cookieAllowlist) {
validateAllowlistPatterns(config.cookieAllowlist, 'cookie');
}
if (config.headerAllowlist) {
validateAllowlistPatterns(config.headerAllowlist, 'header');
}
}