UNPKG

@himorishige/noren-plugin-security

Version:

Security-focused plugin for Noren (JWT tokens, API keys, HTTP headers, cookies)

221 lines (220 loc) 8.75 kB
import { isCookieAllowed, logSecurityError, parseCookieHeader, parseSetCookieHeader, } from './utils.js'; /** * Generic token masking with configurable preserve length */ function maskToken(token, preserveStart = 4, preserveEnd = 4, minLength = 8) { if (token.length <= minLength) { return '*'.repeat(token.length); } const actualStart = Math.min(preserveStart, Math.floor(token.length / 3)); const actualEnd = Math.min(preserveEnd, Math.floor(token.length / 3)); const maskLength = Math.max(1, token.length - actualStart - actualEnd); return (token.substring(0, actualStart) + '*'.repeat(maskLength) + token.substring(token.length - actualEnd)); } /** Mask JWT while preserving structure */ function maskJwtStructure(jwt) { const parts = jwt.split('.'); if (parts.length !== 3) return '[REDACTED:JWT]'; return parts.map((part) => maskToken(part, 3, 3, 6)).join('.'); } /** Mask API key while preserving prefix */ function maskApiKey(apiKey) { const prefixMatch = apiKey.match(/^([a-z]+_)(.+)$/i); if (prefixMatch) { const [, prefix, suffix] = prefixMatch; return prefix + '*'.repeat(Math.max(4, suffix.length)); } return maskToken(apiKey); } /** Mask tokens with standard pattern */ function maskStandardToken(token) { return maskToken(token); } /** Mask session IDs */ function maskSessionId(sessionId) { const equalIndex = sessionId.indexOf('='); if (equalIndex === -1) return '[REDACTED:SESSION]'; const name = sessionId.substring(0, equalIndex + 1); const value = sessionId.substring(equalIndex + 1); return name + maskToken(value, 3, 3, 6); } // === NEW ENHANCED MASKING FUNCTIONS === /** Mask GitHub tokens while preserving prefix */ function maskGithubToken(token) { const match = token.match(/^(gh[opusa]_)(.+)$/); if (match) { const [, prefix, suffix] = match; return prefix + '*'.repeat(Math.max(8, suffix.length)); } return '[REDACTED:GITHUB-TOKEN]'; } /** Mask AWS Access Key while preserving prefix */ function maskAwsAccessKey(key) { const match = key.match(/^((?:AKIA|ASIA|AGPA|AIDA|ANPA|AROA|AIPA))(.+)$/); if (match) { const [, prefix, suffix] = match; return prefix + '*'.repeat(Math.max(6, suffix.length)); } return '[REDACTED:AWS-ACCESS-KEY]'; } /** Mask Google API Key while preserving prefix */ function maskGoogleApiKey(key) { const match = key.match(/^(AIza)(.+)$/); if (match) { const [, prefix, suffix] = match; return prefix + '*'.repeat(Math.max(8, suffix.length)); } return '[REDACTED:GOOGLE-API-KEY]'; } /** Mask Stripe API Key while preserving prefix and environment */ function maskStripeApiKey(key) { const match = key.match(/^((sk|pk)_(live|test)_)(.+)$/); if (match) { const [, prefix, , , suffix] = match; return prefix + '*'.repeat(Math.max(8, suffix.length)); } return '[REDACTED:STRIPE-API-KEY]'; } /** Mask Slack token while preserving prefix */ function maskSlackToken(token) { const match = token.match(/^(xox[abps]-\d+-\d+-)(.+)$|^(xapp-)(.+)$/); if (match) { const prefix = match[1] || match[3]; const suffix = match[2] || match[4]; return prefix + '*'.repeat(Math.max(6, suffix.length)); } return '[REDACTED:SLACK-TOKEN]'; } /** Mask SendGrid API Key while preserving structure */ function maskSendGridApiKey(key) { const match = key.match(/^(SG\.)([^.]+)\.(.+)$/); if (match) { const [, prefix, middle, suffix] = match; return `${prefix}${'*'.repeat(Math.max(4, middle.length))}.${'*'.repeat(Math.max(4, suffix.length))}`; } return '[REDACTED:SENDGRID-API-KEY]'; } /** Mask OpenAI API Key while preserving prefix */ function maskOpenAiApiKey(key) { const match = key.match(/^(sk-(?:proj-)?)(.+)$/); if (match) { const [, prefix, suffix] = match; return prefix + '*'.repeat(Math.max(8, suffix.length)); } return '[REDACTED:OPENAI-API-KEY]'; } /** Mask Google OAuth tokens while preserving prefix */ function maskGoogleOAuthToken(token) { const match = token.match(/^(ya29\.)(.+)$|^(1\/\/)(.+)$/); if (match) { const prefix = match[1] || match[3]; const suffix = match[2] || match[4]; return prefix + '*'.repeat(Math.max(8, suffix.length)); } return '[REDACTED:GOOGLE-OAUTH-TOKEN]'; } /** Mask Azure subscription key */ function maskAzureSubscriptionKey(key) { return maskToken(key, 4, 4, 8); } /** Mask Webhook URLs while preserving domain */ function maskWebhookUrl(url) { try { const urlObj = new URL(url); const path = urlObj.pathname; const pathParts = path.split('/'); // Mask the sensitive token parts while keeping structure if (pathParts.length >= 3) { const maskedParts = pathParts.map((part, index) => { if (index <= 2 || part.length < 8) return part; return '*'.repeat(Math.min(12, part.length)); }); return `${urlObj.protocol}//${urlObj.host}${maskedParts.join('/')}`; } } catch { // Fallback for invalid URLs } return '[REDACTED:WEBHOOK-URL]'; } /** Mask signed URLs while preserving base URL */ function maskSignedUrl(url) { try { const urlObj = new URL(url); return `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}?[REDACTED:SIGNED-PARAMS]`; } catch { return '[REDACTED:SIGNED-URL]'; } } /** * Generic cookie header masker with error handling */ function maskCookieWithErrorHandling(headerValue, config, parseFunc, isSetCookie = false) { try { if (isSetCookie) { const cookie = parseFunc(headerValue); if (!cookie) return '[REDACTED:SET-COOKIE]'; if (isCookieAllowed(cookie.name, config)) return headerValue; const maskedValue = maskToken(cookie.value, 2, 2, 6); return headerValue.replace(/^(Set-Cookie\s*:\s*[^=]+=)([^;]+)(.*)$/i, `$1${maskedValue}$3`); } else { const cookies = parseFunc(headerValue); const maskedPairs = cookies.map((cookie) => isCookieAllowed(cookie.name, config) ? `${cookie.name}=${cookie.value}` : `${cookie.name}=${maskToken(cookie.value, 2, 2, 6)}`); return `Cookie: ${maskedPairs.join('; ')}`; } } catch (error) { const errorObj = error instanceof Error ? error : new Error(String(error)); logSecurityError(`${isSetCookie ? 'Set-Cookie' : 'Cookie'} masking`, errorObj, headerValue); return `[REDACTED:${isSetCookie ? 'SET-COOKIE' : 'COOKIE'}]`; } } /** Mask Cookie header with allowlist consideration */ function maskCookieHeader(cookieHeader, config) { return maskCookieWithErrorHandling(cookieHeader, config, parseCookieHeader, false); } /** Mask Set-Cookie header with allowlist consideration */ function maskSetCookieHeader(setCookieHeader, config) { return maskCookieWithErrorHandling(setCookieHeader, config, parseSetCookieHeader, true); } /** Create masker functions with configuration */ export function createSecurityMaskers(config) { return { // Existing maskers sec_jwt_token: (h) => maskJwtStructure(h.value), sec_api_key: (h) => maskApiKey(h.value), sec_uuid_token: (h) => maskStandardToken(h.value), sec_hex_token: (h) => maskStandardToken(h.value), sec_session_id: (h) => maskSessionId(h.value), sec_auth_header: () => '[REDACTED:AUTH]', sec_cookie: (h) => maskCookieHeader(h.value, config), sec_set_cookie: (h) => maskSetCookieHeader(h.value, config), sec_url_token: () => '[REDACTED:URL-TOKEN]', sec_client_secret: () => '[REDACTED:CLIENT-SECRET]', // === NEW ENHANCED MASKERS === sec_github_token: (h) => maskGithubToken(h.value), sec_aws_access_key: (h) => maskAwsAccessKey(h.value), sec_google_api_key: (h) => maskGoogleApiKey(h.value), sec_stripe_api_key: (h) => maskStripeApiKey(h.value), sec_slack_token: (h) => maskSlackToken(h.value), sec_sendgrid_api_key: (h) => maskSendGridApiKey(h.value), sec_openai_api_key: (h) => maskOpenAiApiKey(h.value), sec_google_oauth_token: (h) => maskGoogleOAuthToken(h.value), sec_azure_subscription_key: (h) => maskAzureSubscriptionKey(h.value), sec_webhook_url: (h) => maskWebhookUrl(h.value), sec_signed_url: (h) => maskSignedUrl(h.value), }; } /** Default security maskers without configuration */ export const maskers = createSecurityMaskers();