Skip to main content
Glama
cookie-utils.ts9.61 kB
/* * This file is part of BrowserLoop. * * BrowserLoop is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * BrowserLoop is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with BrowserLoop. If not, see <https://www.gnu.org/licenses/>. */ import { z } from 'zod'; import type { Cookie } from './types.js'; /** * Zod schema for validating cookie objects * Enhanced with security validation to prevent injection attacks * Supports browser extension cookie export format */ const CookieSchema = z.object({ name: z .string() .min(1, 'Cookie name cannot be empty') .max(255, 'Cookie name too long') .regex( /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/, 'Cookie name contains invalid characters' ), value: z.string().max(4096, 'Cookie value too long'), // RFC limit domain: z .string() .max(255, 'Cookie domain too long') .regex(/^[a-zA-Z0-9.-]*$/, 'Cookie domain contains invalid characters') .optional(), path: z.string().max(4096, 'Cookie path too long').optional(), httpOnly: z.boolean().optional(), secure: z.boolean().optional(), expires: z .number() .min(-1, 'Cookie expires must be -1 (session) or positive timestamp') // Allow -1 for session cookies .max(2147483647, 'Cookie expires timestamp too large') // 32-bit timestamp limit .optional(), sameSite: z.enum(['Strict', 'Lax', 'None']).optional(), }); /** * Parse and validate cookies from input (array or JSON string) * Never logs cookie values for security */ export function parseCookies(input: Cookie[] | string): Cookie[] { try { let cookieArray: unknown[]; // First, handle the input type and convert to array if (typeof input === 'string') { // Validate string constraints z.string() .min(1, 'Cookie JSON string cannot be empty') .max(51200, 'Cookie JSON string too large (50KB limit)') .parse(input); // Parse JSON string const parsed = JSON.parse(input); if (!Array.isArray(parsed)) { throw new Error( 'Cookie parsing failed: Cookie JSON must be an array of cookie objects' ); } cookieArray = parsed; } else if (Array.isArray(input)) { cookieArray = input; } else { throw new Error('Invalid input: cookies must be an array or JSON string'); } // Validate array length if (cookieArray.length > 50) { throw new Error('Too many cookies (maximum 50)'); } // Validate each cookie individually to get specific error messages const validatedCookies = cookieArray.map((cookie) => { try { return CookieSchema.parse(cookie); } catch (error) { if (error instanceof z.ZodError) { const issues = error.errors .map((issue) => `${issue.path.join('.')}: ${issue.message}`) .join(', '); throw new Error(`Cookie validation failed: ${issues}`); } throw error; } }); return validatedCookies as Cookie[]; } catch (error) { if (error instanceof z.ZodError) { const issues = error.errors .map((issue) => `${issue.path.join('.')}: ${issue.message}`) .join(', '); throw new Error(`Cookie validation failed: ${issues}`); } if (error instanceof SyntaxError) { throw new Error('Invalid JSON format in cookies parameter'); } throw error; } } /** * Sanitize cookies for logging (removes sensitive values) * SECURITY: Never logs actual cookie values */ export function sanitizeCookiesForLogging(cookies: Cookie[]): object[] { return cookies.map((cookie) => ({ name: cookie.name, domain: cookie.domain || '[auto-derived]', path: cookie.path || '/', httpOnly: cookie.httpOnly, secure: cookie.secure, expires: cookie.expires, sameSite: cookie.sameSite, valueLength: cookie.value.length, hasValue: cookie.value.length > 0, })); } /** * Validate and sanitize cookies input * SECURITY: Returns sanitized version for logging */ export function validateAndSanitize(input: Cookie[] | string): { cookies: Cookie[]; sanitizedForLogging: object[]; } { const cookies = parseCookies(input); const sanitizedForLogging = sanitizeCookiesForLogging(cookies); return { cookies, sanitizedForLogging }; } /** * Check if input looks like a cookie array or JSON string */ export function isValidCookieInput(input: unknown): boolean { try { parseCookies(input as Cookie[] | string); return true; } catch { return false; } } /** * Clear sensitive cookie data from memory * SECURITY: Overwrites cookie values to prevent memory dumps */ export function clearCookieMemory(cookies: Cookie[]): void { if (!cookies || !Array.isArray(cookies)) { return; } for (const cookie of cookies) { if (cookie && typeof cookie === 'object') { // Overwrite sensitive fields with random data first, then empty if (cookie.value) { (cookie as { value: string }).value = Math.random().toString(36); (cookie as { value: string }).value = ''; } if (cookie.expires) { (cookie as { expires: number }).expires = 0; } } } } /** * Validate cookie security requirements * SECURITY: Enforces secure cookie practices */ export function validateCookieSecurity(cookies: Cookie[]): void { for (const cookie of cookies) { // Warn about insecure cookies (dev-only warning) if (cookie.value && cookie.value.length > 100 && !cookie.httpOnly) { // Note: This is just validation, no logging of actual values } // Check for suspicious patterns that might indicate injection if ( containsSuspiciousPatterns(cookie.name) || containsSuspiciousPatterns(cookie.domain || '') ) { throw new Error(`Cookie contains suspicious patterns: ${cookie.name}`); } } } /** * Check for suspicious patterns that might indicate injection attacks */ export function containsSuspiciousPatterns(value: string): boolean { const suspiciousPatterns = [ /<script/i, /javascript:/i, /data:/i, /vbscript:/i, /on\w+=/i, /document\./i, /window\./i, /eval\(/i, /setTimeout\(/i, /setInterval\(/i, ]; return suspiciousPatterns.some((pattern) => pattern.test(value)); } /** * Filter cookies based on domain matching with target URL * Uses RFC 6265 domain matching rules to determine valid cookies * @param cookies - Array of cookies to filter * @param targetUrl - Target URL to match against * @returns Object with matching cookies and count of filtered cookies */ export function filterCookiesByDomain( cookies: Cookie[], targetUrl: string ): { matchingCookies: Cookie[]; filteredCount: number } { if (!cookies || cookies.length === 0) { return { matchingCookies: [], filteredCount: 0 }; } const urlObj = new URL(targetUrl); const targetDomain = urlObj.hostname.toLowerCase(); const matchingCookies: Cookie[] = []; let filteredCount = 0; for (const cookie of cookies) { // Skip domain validation for __Host- cookies (they must not have domains) if (cookie.name.startsWith('__Host-')) { matchingCookies.push(cookie); continue; } // If cookie has no domain, it will be auto-derived and is always valid if (!cookie.domain) { matchingCookies.push(cookie); continue; } const cookieDomain = cookie.domain.toLowerCase(); if (isDomainValidForTarget(cookieDomain, targetDomain)) { matchingCookies.push(cookie); } else { filteredCount++; } } return { matchingCookies, filteredCount }; } /** * Check if a cookie domain is valid for a target domain according to RFC 6265 * This is a duplicate of the logic in ScreenshotService.isDomainValid() * to enable cookie filtering without requiring service instance * @param cookieDomain - The domain specified in the cookie * @param targetDomain - The domain from the URL * @returns true if the cookie domain is valid for the target domain */ function isDomainValidForTarget( cookieDomain: string, targetDomain: string ): boolean { // Exact match if (cookieDomain === targetDomain) { return true; } // Handle localhost and IP addresses specially if (targetDomain === 'localhost' || targetDomain === '127.0.0.1') { return ( cookieDomain === 'localhost' || cookieDomain === '127.0.0.1' || cookieDomain === '.localhost' ); } // Handle domain with leading dot (parent domain) if (cookieDomain.startsWith('.')) { const parentDomain = cookieDomain.slice(1); // Remove leading dot // Cookie domain ".example.com" is valid for "app.example.com" // but not for "example.com" itself (RFC 6265 requirement) if (targetDomain === parentDomain) { return false; // Parent domain cookie cannot be set on the parent domain itself } // Check if target domain is a subdomain of the cookie domain return targetDomain.endsWith(`.${parentDomain}`); } // Handle subdomain cookie for exact domain match if (targetDomain.startsWith('.')) { return targetDomain === `.${cookieDomain}`; } return false; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mattiasw/browserloop'

If you have feedback or need assistance with the MCP directory API, please join our Discord server