Skip to content

Commit 52b9a3f

Browse files
authored
Sanitize connector filenames (#277)
1 parent 2348000 commit 52b9a3f

6 files changed

Lines changed: 62 additions & 30 deletions

File tree

ee/services/connectors/base_connector.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import hashlib
2+
import re
13
from abc import ABC, abstractmethod
24
from io import BytesIO
3-
from typing import Any, Dict, Optional
5+
from pathlib import Path
6+
from typing import Any, Dict, Optional, Union
47

58
from pydantic import BaseModel
69

@@ -27,6 +30,28 @@ class BaseConnector(ABC):
2730
@abstractmethod
2831
def __init__(self, user_morphik_id: str):
2932
self.user_morphik_id = user_morphik_id
33+
# Safe identifier for filesystem use (no path separators or traversal sequences)
34+
self.user_storage_id = self._sanitize_user_identifier(user_morphik_id)
35+
36+
@staticmethod
37+
def _sanitize_user_identifier(user_morphik_id: str) -> str:
38+
"""Return a filesystem-safe identifier with collision resistance."""
39+
safe_id = re.sub(r"[^A-Za-z0-9_-]", "_", user_morphik_id).strip("_")[:32]
40+
hash_suffix = hashlib.sha256(user_morphik_id.encode("utf-8")).hexdigest()[:8]
41+
if not safe_id:
42+
return f"user_{hash_suffix}"
43+
return f"{safe_id}_{hash_suffix}"
44+
45+
def _build_storage_path(self, base_dir: Union[str, Path], prefix: str, suffix: str) -> Path:
46+
"""Construct a normalized, directory-confined path for storing connector secrets."""
47+
base_path = Path(base_dir)
48+
base_path.mkdir(parents=True, exist_ok=True)
49+
filename = f"{prefix}{self.user_storage_id}{suffix}"
50+
base_resolved = base_path.resolve()
51+
full_path = (base_resolved / filename).resolve()
52+
if base_resolved not in full_path.parents and full_path != base_resolved:
53+
raise ValueError("Unsafe storage path resolved outside of configured directory")
54+
return full_path
3055

3156
@abstractmethod
3257
async def get_auth_status(self) -> ConnectorAuthStatus:

ee/services/connectors/github_connector.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import logging
22
import mimetypes
3-
import os
43
from io import BytesIO
54
from pathlib import Path
65
from typing import TYPE_CHECKING, Any, Dict, List, Optional
@@ -32,9 +31,7 @@ def __init__(self, user_morphik_id: str):
3231
self._load_credentials()
3332

3433
def _get_user_token_path(self) -> Path:
35-
token_dir = Path(self.ee_settings.GITHUB_TOKEN_STORAGE_PATH)
36-
os.makedirs(token_dir, exist_ok=True)
37-
return token_dir / f"github_token_{self.user_morphik_id}.txt"
34+
return self._build_storage_path(self.ee_settings.GITHUB_TOKEN_STORAGE_PATH, "github_token_", ".txt")
3835

3936
def _save_credentials(self, access_token: str) -> None:
4037
token_path = self._get_user_token_path()

ee/services/connectors/google_drive_connector.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# This is the beginning of the GoogleDriveConnector implementation.
22

33
import logging
4-
import os
54
import pickle
65
from io import BytesIO
76
from pathlib import Path
@@ -33,10 +32,7 @@ def __init__(self, user_morphik_id: str):
3332
self._load_credentials()
3433

3534
def _get_user_token_path(self) -> Path:
36-
token_dir = Path(self.ee_settings.GOOGLE_TOKEN_STORAGE_PATH)
37-
# Create the token directory if it doesn't exist
38-
os.makedirs(token_dir, exist_ok=True)
39-
return token_dir / f"gdrive_token_{self.user_morphik_id}.pickle"
35+
return self._build_storage_path(self.ee_settings.GOOGLE_TOKEN_STORAGE_PATH, "gdrive_token_", ".pickle")
4036

4137
def _save_credentials(self) -> None:
4238
if not self.creds:

ee/services/connectors/zotero_connector.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import json
22
import logging
3-
import os
43
from io import BytesIO
54
from pathlib import Path
65
from typing import Any, Dict, List, Optional
@@ -27,9 +26,7 @@ def __init__(self, user_morphik_id: str):
2726
def _get_user_credentials_path(self) -> Path:
2827
"""Get the path to store user credentials."""
2928
# Use the same directory structure as Google Drive but for Zotero
30-
token_dir = Path(self.ee_settings.GOOGLE_TOKEN_STORAGE_PATH)
31-
os.makedirs(token_dir, exist_ok=True)
32-
return token_dir / f"zotero_creds_{self.user_morphik_id}.json"
29+
return self._build_storage_path(self.ee_settings.GOOGLE_TOKEN_STORAGE_PATH, "zotero_creds_", ".json")
3330

3431
def _save_credentials(self) -> None:
3532
"""Save credentials to file."""

ee/ui-component/lib/pdf-commands.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,21 @@ if (!globalThis.pdfSessions) {
2828

2929
const sessions: Map<string, PDFSession> = globalThis.pdfSessions;
3030

31+
const MAX_LOG_VALUE_LENGTH = 200;
32+
33+
function sanitizeForLog(value: string): string {
34+
if (!value) return "";
35+
const trimmed = value.replace(/[\r\n\t]+/g, " ").replace(/[^\x20-\x7E]/g, "");
36+
return trimmed.length > MAX_LOG_VALUE_LENGTH ? `${trimmed.slice(0, MAX_LOG_VALUE_LENGTH)}...` : trimmed;
37+
}
38+
3139
// Clean up inactive sessions (older than 1 hour)
3240
function cleanupInactiveSessions() {
3341
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
3442
for (const [sessionId, session] of Array.from(sessions.entries())) {
3543
if (session.lastActivity < oneHourAgo) {
36-
console.log(`Cleaning up inactive session: ${sessionId}`);
44+
const safeSessionId = sanitizeForLog(sessionId);
45+
console.log("Cleaning up inactive session:", safeSessionId);
3746
// Close all clients in the session
3847
session.clients.forEach((client: ReadableStreamDefaultController) => {
3948
try {
@@ -51,6 +60,8 @@ function cleanupInactiveSessions() {
5160
setInterval(cleanupInactiveSessions, 30 * 60 * 1000);
5261

5362
export function getOrCreateSession(sessionId: string, userId: string): PDFSession {
63+
const safeSessionId = sanitizeForLog(sessionId);
64+
const safeUserId = sanitizeForLog(userId);
5465
let session = sessions.get(sessionId);
5566

5667
if (!session) {
@@ -62,7 +73,7 @@ export function getOrCreateSession(sessionId: string, userId: string): PDFSessio
6273
lastActivity: new Date(),
6374
};
6475
sessions.set(sessionId, session);
65-
console.log(`Created new PDF session: ${sessionId} for user: ${userId}`);
76+
console.log("Created new PDF session", safeSessionId, "for user", safeUserId);
6677
} else {
6778
// Verify user owns this session (allow anonymous/authenticated user mixing for development)
6879
const isUserMatch =
@@ -84,20 +95,22 @@ export function getOrCreateSession(sessionId: string, userId: string): PDFSessio
8495
}
8596

8697
export function addClient(controller: ReadableStreamDefaultController, sessionId: string, userId: string) {
87-
console.log(`Adding new PDF API client for session: ${sessionId}, user: ${userId}`);
98+
const safeSessionId = sanitizeForLog(sessionId);
99+
const safeUserId = sanitizeForLog(userId);
100+
console.log("Adding new PDF API client for session", safeSessionId, "user", safeUserId);
88101

89102
const session = getOrCreateSession(sessionId, userId);
90103
session.clients.push(controller);
91104

92-
console.log(`Total clients in session ${sessionId}:`, session.clients.length);
105+
console.log("Total clients in session", safeSessionId, session.clients.length);
93106

94107
// Send initial connection message
95108
const connectionMessage = `data: ${JSON.stringify({ type: "connected", sessionId, userId })}\n\n`;
96109
console.log("Sending connection message:", connectionMessage);
97110
controller.enqueue(connectionMessage);
98111

99112
// Send any queued commands for this session
100-
console.log(`Sending queued commands for session ${sessionId}:`, session.commandQueue.length);
113+
console.log("Sending queued commands for session", safeSessionId, session.commandQueue.length);
101114
session.commandQueue.forEach(command => {
102115
const message = `data: ${JSON.stringify(command)}\n\n`;
103116
console.log("Sending queued command:", message);
@@ -109,18 +122,19 @@ export function addClient(controller: ReadableStreamDefaultController, sessionId
109122
}
110123

111124
export function removeClient(controller: ReadableStreamDefaultController, sessionId: string) {
112-
console.log(`Removing PDF API client from session: ${sessionId}`);
125+
const safeSessionId = sanitizeForLog(sessionId);
126+
console.log("Removing PDF API client from session", safeSessionId);
113127

114128
const session = sessions.get(sessionId);
115129
if (!session) {
116-
console.log(`Session ${sessionId} not found`);
130+
console.log("Session not found", safeSessionId);
117131
return;
118132
}
119133

120134
const index = session.clients.indexOf(controller);
121135
if (index > -1) {
122136
session.clients.splice(index, 1);
123-
console.log(`Client removed from session ${sessionId}. Remaining clients:`, session.clients.length);
137+
console.log("Client removed from session", safeSessionId, "Remaining clients:", session.clients.length);
124138

125139
// If no clients left in session, we could optionally clean it up
126140
// For now, we'll keep it for a while in case the client reconnects
@@ -131,11 +145,13 @@ export function removeClient(controller: ReadableStreamDefaultController, sessio
131145

132146
// Function to broadcast commands to all connected clients in a specific session
133147
export function broadcastPDFCommand(command: PDFCommand, sessionId: string, userId: string) {
134-
console.log(`Broadcasting PDF command to session ${sessionId}:`, command);
148+
const safeSessionId = sanitizeForLog(sessionId);
149+
const safeUserId = sanitizeForLog(userId);
150+
console.log("Broadcasting PDF command to session", safeSessionId, "user", safeUserId, command);
135151

136152
const session = sessions.get(sessionId);
137153
if (!session) {
138-
console.log(`Session ${sessionId} not found, creating new session`);
154+
console.log("Session not found, creating new session", safeSessionId, "user", safeUserId);
139155
getOrCreateSession(sessionId, userId);
140156
return broadcastPDFCommand(command, sessionId, userId);
141157
}
@@ -166,22 +182,22 @@ export function broadcastPDFCommand(command: PDFCommand, sessionId: string, user
166182
// Send to all connected clients in this session
167183
session.clients.forEach((controller, index) => {
168184
try {
169-
console.log(`Sending command to client ${index + 1} in session ${sessionId}:`, message);
185+
console.log("Sending command to client", index + 1, "in session", safeSessionId, message);
170186
controller.enqueue(message);
171187
} catch (error) {
172-
console.error(`Error sending command to client ${index + 1} in session ${sessionId}:`, error);
188+
console.error("Error sending command to client", index + 1, "in session", safeSessionId, error);
173189
// Remove failed client
174190
const clientIndex = session.clients.indexOf(controller);
175191
if (clientIndex > -1) {
176192
session.clients.splice(clientIndex, 1);
177-
console.log(`Removed failed client from session ${sessionId}. Remaining clients: ${session.clients.length}`);
193+
console.log("Removed failed client from session", safeSessionId, "Remaining clients:", session.clients.length);
178194
}
179195
}
180196
});
181197

182198
// If no clients connected in this session, queue the command
183199
if (session.clients.length === 0) {
184-
console.log(`No clients connected in session ${sessionId}, queueing command`);
200+
console.log("No clients connected in session", safeSessionId, "queueing command");
185201
session.commandQueue.push(scopedCommand);
186202
// Keep only the last 10 commands to prevent memory issues
187203
if (session.commandQueue.length > 10) {

scripts/check_completeness.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,9 @@ async def main():
332332
print(f"Using default namespace: {turbopuffer_namespace}")
333333
print("(You can specify a different namespace as: python check_completeness.py <namespace>)")
334334

335-
print(f"Using Supabase URI: {settings.POSTGRES_URI}")
336-
print(f"Using TurboPuffer API key: {settings.TURBOPUFFER_API_KEY[:10]}...")
335+
# Avoid printing secrets directly; just confirm they are configured.
336+
print("Using Supabase URI: [configured]")
337+
print("Using TurboPuffer API key: [configured]")
337338

338339
print("Fetching data from Supabase...")
339340
try:

0 commit comments

Comments
 (0)