Skip to main content
Glama
by clipsense
SECURITY_AUDIT_REPORT.mdโ€ข25.7 kB
# ClipSense Security Audit Report ## OWASP Top 10 Security Assessment **Date:** December 5, 2025 **Assessment Scope:** ClipSense MCP Server (TypeScript/Node.js Client) **Framework:** OWASP Top 10 2021 --- ## Executive Summary This security audit covers the ClipSense MCP Server - a Node.js/TypeScript client that connects to the ClipSense backend API for video analysis. The MCP server itself is a minimal client implementation that communicates with a remote API via HTTPS. While the code follows some security best practices, several issues were identified ranging from HIGH to MEDIUM severity, primarily related to credential handling, input validation, and information disclosure. **Critical Findings:** 2 **High Findings:** 3 **Medium Findings:** 4 **Low Findings:** 2 --- ## Detailed Findings ### CRITICAL ISSUES #### 1. CRITICAL: Hardcoded API Keys in Test Files **Severity:** CRITICAL **OWASP Category:** A02:2021 - Cryptographic Failures **Location:** - `/Users/jerlitaburanday/clipsense-mcp-server/test_api.py` (Line 9) - `/Users/jerlitaburanday/clipsense-mcp-server/test_video_analysis.py` (Line 8) - `/Users/jerlitaburanday/clipsense-mcp-server/test_video_2.py` (N/A - imported constant) - `/Users/jerlitaburanday/clipsense-mcp-server/test_video_3.py` (N/A - imported constant) - `/Users/jerlitaburanday/clipsense-mcp-server/reset_usage_direct.py` (Line 33, 43) **Description:** Production API keys are hardcoded directly in Python test files. The key `cs_sk_pNQhgId_0X8P-gt010CkRfZ4cgVVAejH9JQj_LpPmYg` appears in multiple test scripts and in commented SQL query examples. **Proof of Concept:** ```python # test_api.py:9 API_KEY = "cs_sk_pNQhgId_0X8P-gt010CkRfZ4cgVVAejH9JQj_LpPmYg" # test_video_analysis.py:8 API_KEY = "cs_sk_pNQhgId_0X8P-gt010CkRfZ4cgVVAejH9JQj_LpPmYg" # reset_usage_direct.py:33, 43 api_key = "cs_sk_pNQhgId_0X8P-gt010CkRfZ4cgVVAejH9JQj_LpPmYg" ``` **Impact:** - Anyone with access to the repository (GitHub, local clone, CI/CD logs) can access the API - API key can be used to perform unauthorized video analyses - Potential to exceed rate limits and incur costs - Could be used to access other users' data if multi-tenancy is not properly enforced **Recommended Fix:** 1. Immediately rotate the exposed API key in production 2. Remove all hardcoded keys from code 3. Use environment variables for all test scripts: ```python import os API_KEY = os.environ.get("CLIPSENSE_API_KEY") if not API_KEY: raise ValueError("CLIPSENSE_API_KEY environment variable not set") ``` 4. Add `.env` and `*.env.local` to `.gitignore` 5. Implement pre-commit hooks to prevent secrets in commits: ```bash npm install husky --save-dev npm install detect-secrets --save-dev ``` 6. Scan git history for any leaked keys: ```bash git log --all -S "cs_sk_" --oneline ``` --- #### 2. CRITICAL: Database URL Exposure in Code **Severity:** CRITICAL **OWASP Category:** A02:2021 - Cryptographic Failures **Location:** `/Users/jerlitaburanday/clipsense-mcp-server/reset_usage_direct.py` (Line 18-19, 33) **Description:** The script accepts and processes DATABASE_URL containing plaintext PostgreSQL credentials. The script also displays example SQL with hardcoded API key in comments. **Code:** ```python # Line 18-19: Comment with database URL format DATABASE_URL = input("Enter Railway DATABASE_URL (or press Enter to skip): ").strip() # Line 33: Hardcoded key in SQL example print(" WHERE key = 'cs_sk_pNQhgId_0X8P-gt010CkRfZ4cgVVAejH9JQj_LpPmYg';") ``` **Impact:** - Database credentials are exposed in plaintext via terminal input - If this script is logged in CI/CD systems, credentials become visible - Potential for SQL injection if user input is not properly validated - Full database access available to anyone with the URL **Recommended Fix:** 1. Use environment variables for database credentials: ```python import os from urllib.parse import urlparse db_url = os.environ.get("DATABASE_URL") if not db_url: raise ValueError("DATABASE_URL not set") ``` 2. Never store credentials in repository or logs 3. Use Railway's secret management: ```bash railway variable set DATABASE_URL "postgresql://..." ``` 4. Remove hardcoded keys from all SQL examples 5. Implement parameterized queries (already done with psycopg2 `%s` placeholders - good!) --- ### HIGH SEVERITY ISSUES #### 3. HIGH: Arbitrary File Read via Path Traversal **Severity:** HIGH **OWASP Category:** A01:2021 - Broken Access Control **Location:** `/Users/jerlitaburanday/clipsense-mcp-server/src/client.ts` (Line 32-40) **Description:** The `analyzeVideo()` function accepts a `videoPath` parameter directly from user input without validating it against path traversal attacks. While the code uses `basename()` for the filename, it doesn't validate the actual file being read. **Code:** ```typescript // src/client.ts:27-40 async analyzeVideo(videoPath: string, question: string): Promise<...> { // Validates file size but not path safety const stats = statSync(videoPath); if (stats.size > MAX_FILE_SIZE) { throw new Error(`Video file too large...`); } // User can provide any path - only basename is used for upload const fileStream = createReadStream(videoPath); await axios.put(upload_url, fileStream, ...); } ``` **Proof of Concept:** An attacker could attempt to analyze: ```typescript // Access system files "/etc/passwd" "/etc/shadow" "../../../../../../etc/hosts" "~/.ssh/id_rsa" "~/.bash_history" ``` **Impact:** - Unauthorized file access (confidentiality breach) - Potential to read sensitive files from the user's system - Could expose private SSH keys, credentials, or personal data - Violates principle of least privilege **Recommended Fix:** ```typescript import { resolve } from "path"; import { access, constants } from "fs/promises"; async analyzeVideo(videoPath: string, question: string): Promise<...> { // 1. Resolve to absolute path and check for path traversal const resolvedPath = resolve(videoPath); // 2. Only allow files in expected directories const allowedDirs = [ resolve(process.cwd()), resolve(require("os").homedir(), "Desktop"), resolve(require("os").homedir(), "Downloads"), // Add other safe directories as needed ]; const isAllowed = allowedDirs.some(dir => resolvedPath.startsWith(dir)); if (!isAllowed) { throw new Error(`Access denied: File outside allowed directories`); } // 3. Verify file exists and is readable try { await access(resolvedPath, constants.R_OK); } catch { throw new Error(`File not accessible: ${resolvedPath}`); } // 4. Verify it's a regular file (not directory/device) const stats = statSync(resolvedPath); if (!stats.isFile()) { throw new Error(`Path must be a regular file`); } // Continue with existing validation... } ``` --- #### 4. HIGH: Unrestricted Content-Type Upload **Severity:** HIGH **OWASP Category:** A04:2021 - Insecure Deserialization **Location:** `/Users/jerlitaburanday/clipsense-mcp-server/src/client.ts` (Line 110-118) **Description:** The `getContentType()` function accepts any file extension and returns a video MIME type, defaulting to `video/mp4` for unknown extensions. This allows uploading non-video files disguised as videos. **Code:** ```typescript // src/client.ts:135-144 private getContentType(filepath: string): string { const ext = filepath.toLowerCase().split(".").pop(); const types: Record<string, string> = { mp4: "video/mp4", mov: "video/quicktime", webm: "video/webm", avi: "video/x-msvideo", }; return types[ext || ""] || "video/mp4"; // DEFAULT: Returns video/mp4 for unknown types } ``` **Proof of Concept:** ```typescript // An attacker could upload: - malicious.exe โ†’ treated as video/mp4 - payload.php โ†’ treated as video/mp4 - script.sh โ†’ treated as video/mp4 - any_random_file โ†’ treated as video/mp4 ``` **Impact:** - File type validation bypass - Potential upload of malicious files - Could lead to arbitrary code execution if backend processes files unsafely - Violates file upload security best practices **Recommended Fix:** ```typescript private validateAndGetContentType(filepath: string): string { const ext = filepath.toLowerCase().split(".").pop(); const allowedTypes: Record<string, string> = { mp4: "video/mp4", mov: "video/quicktime", webm: "video/webm", avi: "video/x-msvideo", }; // 1. Strict validation - reject unknown types if (!allowedTypes[ext || ""]) { throw new Error( `Invalid file type: .${ext}. Allowed types: mp4, mov, webm, avi` ); } // 2. Verify file magic bytes (first 8 bytes) const magic = this.getFileMagicBytes(filepath); const validMagic = this.validateMagicBytes(magic, ext!); if (!validMagic) { throw new Error(`File content does not match extension`); } return allowedTypes[ext!]; } private getFileMagicBytes(filepath: string): Buffer { const fd = require("fs").openSync(filepath, "r"); const buffer = Buffer.alloc(8); require("fs").readSync(fd, buffer, 0, 8, 0); require("fs").closeSync(fd); return buffer; } private validateMagicBytes(magic: Buffer, ext: string): boolean { // Video format magic bytes const magicPatterns: Record<string, RegExp> = { mp4: /^\x00\x00\x00\x18ftypmp42/, // ftyp header mov: /^\x00\x00\x00\x20ftypqt/, // MOV ftyp webm: /^\x1aEOรกรฒ/, // WEBM signature avi: /^RIFF.*AVI/, // AVI RIFF }; const pattern = magicPatterns[ext]; return pattern ? pattern.test(magic.toString("binary")) : false; } ``` --- #### 5. HIGH: No Input Validation on Question Parameter **Severity:** HIGH **OWASP Category:** A03:2021 - Injection **Location:** `/Users/jerlitaburanday/clipsense-mcp-server/src/client.ts` (Line 32-68) **Description:** The `question` parameter is passed directly to the backend API without validation. While this is not directly a client-side injection vulnerability, it could be exploited if the backend is vulnerable. **Code:** ```typescript // src/index.ts:76-79 const { videoPath, question } = request.params.arguments as { videoPath: string; question?: string; }; // Passed directly without sanitization const result = await client.analyzeVideo(videoPath, question || "..."); // src/client.ts:51-68 const { data: jobData } = await this.client.post("/analyze/start", { video_key: video_key, filename: basename(videoPath), question, // No validation analysis_type: "mobile_bug", }); ``` **Impact:** - Potential injection attacks if backend deserializes improperly - Could be exploited for prompt injection attacks - Violates defense-in-depth principle **Recommended Fix:** ```typescript import { z } from "zod"; // Already in node_modules // Define schema for validation const AnalysisRequestSchema = z.object({ videoPath: z.string() .min(1, "videoPath required") .max(500, "videoPath too long"), question: z.string() .min(1, "question required") .max(1000, "question too long") .regex(/^[a-zA-Z0-9\s.,!?'-]+$/, "question contains invalid characters") .optional() .default("Analyze this bug video and identify the issue."), }); async analyzeVideo(videoPath: string, question: string): Promise<...> { // Validate inputs const validated = AnalysisRequestSchema.parse({ videoPath, question }); // Use validated data const stats = statSync(validated.videoPath); // ... } ``` --- ### MEDIUM SEVERITY ISSUES #### 6. MEDIUM: API Key Stored in Plaintext Configuration File **Severity:** MEDIUM **OWASP Category:** A02:2021 - Cryptographic Failures **Location:** `/Users/jerlitaburanday/clipsense-mcp-server/src/auth.ts` (Line 43-52) **Description:** The API key is stored in plaintext JSON file at `~/.clipsense/config.json` with default permissions. This is readable by any process running as the user. **Code:** ```typescript // src/auth.ts:43-52 async saveApiKey(apiKey: string): Promise<void> { if (!existsSync(CONFIG_DIR)) { mkdirSync(CONFIG_DIR, { recursive: true }); // Default 0755 permissions } const config = { apiKey }; // Written with default permissions - readable by all users in group writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8"); } ``` **File Permissions Issue:** ```bash ~/.clipsense/ โ””โ”€โ”€ config.json (readable by any local user or process) ``` **Impact:** - Local privilege escalation risk - Other users on shared system can read API key - Container/process escape could expose credentials - Violates secure credential storage principles **Recommended Fix:** ```typescript import { chmod } from "fs/promises"; async saveApiKey(apiKey: string): Promise<void> { if (!existsSync(CONFIG_DIR)) { mkdirSync(CONFIG_DIR, { recursive: true }); // Set directory to 700 (rwx------) await chmod(CONFIG_DIR, 0o700); } const config = { apiKey }; const configPath = join(CONFIG_DIR, "config.json"); writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8"); // Set file to 600 (rw-------) await chmod(configPath, 0o600); } ``` Also consider using system keychain: ```typescript // On macOS: use Keychain // On Linux: use GNOME Keyring or pass // On Windows: use Credential Manager import { execSync } from "child_process"; async saveApiKeySecurely(apiKey: string): Promise<void> { const platform = process.platform; if (platform === "darwin") { execSync(`security add-generic-password -a ClipSense -s clipsense-api-key -w "${apiKey}"`); } else if (platform === "linux") { // Use GNOME Keyring or similar execSync(`secret-tool store --label="ClipSense API Key" app clipsense "${apiKey}"`); } else if (platform === "win32") { // Use Windows Credential Manager via PowerShell // Implementation... } } ``` --- #### 7. MEDIUM: No Error Handling Sanitization **Severity:** MEDIUM **OWASP Category:** A01:2021 - Broken Access Control / A09:2021 - Logging & Monitoring **Location:** `/Users/jerlitaburanday/clipsense-mcp-server/src/index.ts` (Line 93-102) **Description:** Error messages are returned directly to the user without sanitization. Sensitive information could be exposed in error responses. **Code:** ```typescript // src/index.ts:93-102 catch (error: any) { return { content: [ { type: "text", // Raw error details exposed text: `โŒ Error: ${error.message}\n\n${error.details || ""}`, }, ], isError: true, }; } ``` **Example Leak:** ``` Error: Connection failed to https://api.clipsense.app:443 Details: socket hang up - ECONNREFUSED 10.0.0.5:443 Trace: at CloudflareR2Storage.uploadFile ``` Could reveal: - Internal IP addresses - Service architecture - Database connection strings (if exposed in errors) - Stack traces with sensitive paths **Recommended Fix:** ```typescript catch (error: any) { // Log full error internally console.error("[AUDIT]", error); // Return sanitized error to user const sanitizedError = this.sanitizeErrorMessage(error); return { content: [ { type: "text", text: `โŒ Error: ${sanitizedError}`, }, ], isError: true, }; } private sanitizeErrorMessage(error: any): string { const message = error?.message || "Unknown error"; // Remove sensitive patterns const patterns = [ /(\d{1,3}\.){3}\d{1,3}/, // IP addresses /postgresql:\/\/.*@/, // Database URLs /@[\w\.\-]+/, // Email addresses /Bearer\s+[\w\-\.]+/, // Tokens /\/Users\/\w+/, // Home paths ]; let sanitized = message; patterns.forEach(pattern => { sanitized = sanitized.replace(pattern, "[REDACTED]"); }); // Only return first 200 characters to prevent information leakage return sanitized.substring(0, 200); } ``` --- #### 8. MEDIUM: No Rate Limiting on Client Side **Severity:** MEDIUM **OWASP Category:** A04:2021 - Insecure Deserialization (DoS aspect) **Location:** `/Users/jerlitaburanday/clipsense-mcp-server/src/client.ts` (Line 27-79) **Description:** The client has no rate limiting. An attacker could hammer the API with repeated requests, causing denial of service or excessive billing. **Code:** ```typescript // src/client.ts - No rate limiting async analyzeVideo(videoPath: string, question: string): Promise<...> { // No throttling, queuing, or rate limit checks const { data: presignData } = await this.client.post("/upload/presign", {...}); // Immediate upload await axios.put(upload_url, fileStream, ...); // Immediate analysis start const { data: jobData } = await this.client.post("/analyze/start", {...}); // No circuit breaker const result = await this.pollJobStatus(jobId); } ``` **Impact:** - DoS vulnerability - user could exhaust their free tier instantly - Runaway costs - could lead to unexpected billing - No protection against rapid-fire requests - No back-off mechanism for failed requests **Recommended Fix:** ```typescript import pLimit from "p-limit"; class ClipSenseClient { private requestQueue = pLimit(1); // Only 1 concurrent request private requestTimestamps: number[] = []; private readonly RATE_LIMIT = 3; // 3 requests per minute private readonly RATE_WINDOW = 60000; // 1 minute private async enforceRateLimit(): Promise<void> { const now = Date.now(); // Remove old timestamps outside the window this.requestTimestamps = this.requestTimestamps.filter( ts => now - ts < this.RATE_WINDOW ); if (this.requestTimestamps.length >= this.RATE_LIMIT) { const oldestRequest = this.requestTimestamps[0]; const waitTime = this.RATE_WINDOW - (now - oldestRequest); throw new Error(`Rate limit exceeded. Wait ${waitTime}ms before next request.`); } this.requestTimestamps.push(now); } async analyzeVideo(videoPath: string, question: string): Promise<...> { // Enforce rate limit and queue requests return this.requestQueue(async () => { await this.enforceRateLimit(); // Existing logic with exponential backoff return this.executeAnalysisWithRetry(videoPath, question); }); } private async executeAnalysisWithRetry( videoPath: string, question: string, attempt: number = 1 ): Promise<...> { const maxRetries = 3; const backoffMs = [1000, 5000, 10000]; try { // Existing implementation return await this.analyzeVideoInternal(videoPath, question); } catch (error: any) { if (attempt < maxRetries && this.isRetryable(error)) { const delay = backoffMs[attempt - 1]; await new Promise(resolve => setTimeout(resolve, delay)); return this.executeAnalysisWithRetry(videoPath, question, attempt + 1); } throw error; } } private isRetryable(error: any): boolean { // Retry on network errors, not on auth/validation errors return error?.code === "ECONNREFUSED" || error?.code === "ETIMEDOUT" || error?.response?.status === 429 || error?.response?.status >= 500; } } ``` --- ### LOW SEVERITY ISSUES #### 9. LOW: Missing HTTPS Certificate Validation Documentation **Severity:** LOW **OWASP Category:** A02:2021 - Cryptographic Failures **Location:** `/Users/jerlitaburanday/clipsense-mcp-server/src/client.ts` (Line 20-26) **Description:** While HTTPS is used, there's no explicit certificate validation or security header configuration documented. **Code:** ```typescript // src/client.ts:20-26 this.client = axios.create({ baseURL: API_BASE_URL, headers: { Authorization: `Bearer ${apiKey}`, }, timeout: 300000, // Missing: rejectUnauthorized, certificatePinning, securityHeaders }); ``` **Impact:** - No protection against MITM attacks (certificate pinning) - Default Node.js behavior is to validate certificates, but not explicitly documented - No CAA (Certification Authority Authorization) records mentioned **Recommended Fix:** ```typescript import https from "https"; import { Agent } from "https"; // Add certificate pinning for production const createSecureClient = () => { const httpsAgent = new Agent({ rejectUnauthorized: true, // Verify certificate chain maxVersion: "TLSv1.3", // Enforce modern TLS minVersion: "TLSv1.2", // Minimum TLS 1.2 // Optional: Certificate pinning (requires cert hash) // pinnedCertificates: [expectedCertHash], }); return axios.create({ baseURL: API_BASE_URL, headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": "ClipSense-MCP/1.0", "Accept": "application/json", "X-Request-ID": generateRequestId(), }, httpsAgent, timeout: 300000, validateStatus: (status) => status < 500, // Explicit status handling }); }; // Document security: /** * Client Configuration: * - HTTPS only with TLS 1.2+ * - Certificate validation enabled * - API key sent in Authorization header * - Request timeout: 5 minutes * - X-Request-ID for request tracking */ ``` --- #### 10. LOW: Missing Request/Response Logging for Audit Trail **Severity:** LOW **OWASP Category:** A09:2021 - Logging and Monitoring Failures **Location:** `/Users/jerlitaburanday/clipsense-mcp-server/src/client.ts` (Overall) **Description:** No logging of API requests/responses for security audit trail. Makes it difficult to investigate security incidents. **Impact:** - Cannot audit who analyzed what - No record of failed authentication attempts - Difficulty debugging security issues - Non-compliant with security audit requirements **Recommended Fix:** ```typescript import winston from "winston"; const logger = winston.createLogger({ level: process.env.LOG_LEVEL || "info", format: winston.format.json(), transports: [ new winston.transports.File({ filename: "error.log", level: "error" }), new winston.transports.File({ filename: "audit.log" }), ], }); // Log requests (without sensitive data) async analyzeVideo(videoPath: string, question: string): Promise<...> { const requestId = generateUUID(); const startTime = Date.now(); logger.info("analyze_video_start", { requestId, fileName: basename(videoPath), fileSize: statSync(videoPath).size, timestamp: new Date().toISOString(), }); try { const result = await this.analyzeVideoInternal(videoPath, question); logger.info("analyze_video_success", { requestId, duration: Date.now() - startTime, jobId: result.jobId, }); return result; } catch (error) { logger.error("analyze_video_error", { requestId, error: error instanceof Error ? error.message : "Unknown error", duration: Date.now() - startTime, }); throw error; } } ``` --- ## Summary Table | # | Issue | Severity | Category | File | Line(s) | |---|-------|----------|----------|------|---------| | 1 | Hardcoded API Keys | CRITICAL | A02:2021 | test_api.py, test_video_analysis.py, etc. | 9, 8, 33, 43 | | 2 | Database URL Exposure | CRITICAL | A02:2021 | reset_usage_direct.py | 18-19, 33 | | 3 | Path Traversal | HIGH | A01:2021 | src/client.ts | 32-40 | | 4 | Unrestricted Content-Type | HIGH | A04:2021 | src/client.ts | 110-118 | | 5 | No Question Validation | HIGH | A03:2021 | src/client.ts, src/index.ts | 32-68, 76-79 | | 6 | Plaintext Config File | MEDIUM | A02:2021 | src/auth.ts | 43-52 | | 7 | Unsafe Error Handling | MEDIUM | A01:2021 | src/index.ts | 93-102 | | 8 | No Rate Limiting | MEDIUM | A04:2021 | src/client.ts | 27-79 | | 9 | Missing HTTPS Config | LOW | A02:2021 | src/client.ts | 20-26 | | 10 | No Audit Logging | LOW | A09:2021 | src/client.ts | Overall | --- ## Remediation Priority ### Immediate (Week 1): 1. Rotate exposed API key (CRITICAL #1) 2. Remove all hardcoded credentials from code 3. Remove database URLs from code (CRITICAL #2) 4. Implement path validation (HIGH #3) ### Short-term (Week 2-3): 5. Add file type validation (HIGH #4) 6. Add input validation (HIGH #5) 7. Secure credential storage (MEDIUM #6) 8. Sanitize error messages (MEDIUM #7) ### Medium-term (Week 4): 9. Implement rate limiting (MEDIUM #8) 10. Add HTTPS security headers (LOW #9) 11. Implement audit logging (LOW #10) --- ## Security Best Practices Implemented Good practices found: - Using Bearer token authentication - HTTPS-only communication - File size validation (500MB limit) - Parameterized database queries (in reset_usage_direct.py) - Timeout configuration (5 minutes) --- ## Recommendations ### General Security Improvements: 1. **Implement Secret Management:** - Use environment variables exclusively - Consider AWS Secrets Manager or Vault for production 2. **Add Testing:** - Add security testing to CI/CD pipeline - Use tools: SAST (SonarQube), dependency scanning (Snyk) 3. **Documentation:** - Add SECURITY.md file with vulnerability disclosure process - Document security architecture and threat model 4. **Dependency Management:** - Regular `npm audit` checks - Keep Node.js and dependencies updated 5. **Access Control:** - Implement API key rotation policies - Add key expiration dates --- ## Conclusion The ClipSense MCP Server is a relatively simple client application. While it follows some security practices, several critical issues related to credential management and input validation were identified. The most urgent priority is immediately rotating the exposed API key and removing all hardcoded credentials from the codebase. The recommended fixes provided in this report should be implemented in priority order, with critical issues addressed within days rather than weeks.

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/clipsense/-mcp-server'

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