testtest123 commited on
Commit
82d183d
·
1 Parent(s): 28dc115

Security Hardening: Fixed path traversal, BM25 RCE, IDOR, and removed backdoor

Browse files
RAG_FULL_APPLICATION_BACKEND/app/main.py CHANGED
@@ -7,18 +7,24 @@ from .utils.ws_manager import ws_manager
7
  import logging
8
  import os
9
 
 
 
 
 
 
10
  # Setup Logger
11
  logging.basicConfig(level=logging.INFO)
12
  logger = logging.getLogger(__name__)
13
 
 
14
  app = FastAPI(title="RAG Pipeline API", version="3.0.0")
 
 
15
 
16
  # CORS
17
- # origins = settings.CORS_ORIGINS.split(",")
18
- origins = settings.CORS_ORIGINS.split(",") if settings.CORS_ORIGINS else ["*"]
19
  app.add_middleware(
20
  CORSMiddleware,
21
- allow_origins=origins,
22
  allow_credentials=True,
23
  allow_methods=["*"],
24
  allow_headers=["*"],
@@ -40,9 +46,14 @@ async def health_check():
40
 
41
  @app.websocket("/ws/pipeline/{job_id}")
42
  async def pipeline_ws(websocket: WebSocket, job_id: str, token: str):
43
- # JWT verification logic will go here
44
- # For now, just connect
45
- await ws_manager.connect(job_id, websocket, "anonymous")
 
 
 
 
 
46
  try:
47
  while True:
48
  data = await websocket.receive_text()
@@ -50,7 +61,7 @@ async def pipeline_ws(websocket: WebSocket, job_id: str, token: str):
50
  except Exception as e:
51
  logger.error(f"WebSocket error for job {job_id}: {e}")
52
  finally:
53
- await ws_manager.disconnect(job_id, "anonymous")
54
 
55
  # Serve frontend static files in production monolith
56
  static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "static")
 
7
  import logging
8
  import os
9
 
10
+ from slowapi import Limiter, _rate_limit_exceeded_handler
11
+ from slowapi.util import get_remote_address
12
+ from slowapi.errors import RateLimitExceeded
13
+ from .utils.auth_utils import decode_token
14
+
15
  # Setup Logger
16
  logging.basicConfig(level=logging.INFO)
17
  logger = logging.getLogger(__name__)
18
 
19
+ limiter = Limiter(key_func=get_remote_address, default_limits=[f"{settings.RATE_LIMIT_PER_MINUTE}/minute"])
20
  app = FastAPI(title="RAG Pipeline API", version="3.0.0")
21
+ app.state.limiter = limiter
22
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
23
 
24
  # CORS
 
 
25
  app.add_middleware(
26
  CORSMiddleware,
27
+ allow_origins=["http://localhost:5174", "http://127.0.0.1:5174"],
28
  allow_credentials=True,
29
  allow_methods=["*"],
30
  allow_headers=["*"],
 
46
 
47
  @app.websocket("/ws/pipeline/{job_id}")
48
  async def pipeline_ws(websocket: WebSocket, job_id: str, token: str):
49
+ # JWT verification
50
+ payload = decode_token(token)
51
+ if not payload:
52
+ await websocket.close(code=1008, reason="Invalid token")
53
+ return
54
+
55
+ user_id = payload.get("id", "anonymous")
56
+ await ws_manager.connect(job_id, websocket, user_id)
57
  try:
58
  while True:
59
  data = await websocket.receive_text()
 
61
  except Exception as e:
62
  logger.error(f"WebSocket error for job {job_id}: {e}")
63
  finally:
64
+ await ws_manager.disconnect(job_id, user_id)
65
 
66
  # Serve frontend static files in production monolith
67
  static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "static")
RAG_FULL_APPLICATION_BACKEND/app/routers/auth.py CHANGED
@@ -44,18 +44,3 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends()):
44
  access_token = create_access_token(data={"sub": user["username"], "id": user["id"]})
45
  return {"access_token": access_token, "token_type": "bearer"}
46
 
47
- @router.post("/seed_admin")
48
- async def seed_admin():
49
- import traceback
50
- try:
51
- hashed = get_password_hash("admin123")
52
- supabase_service.client.table("users").delete().eq("username", "admin").execute()
53
- supabase_service.client.table("users").insert({
54
- "username": "admin",
55
- "password_hash": hashed
56
- }).execute()
57
- logger.info("Admin user seeded successfully.")
58
- return {"msg": "Admin user created/reset (admin / admin123)"}
59
- except Exception as e:
60
- logger.error(f"Seeding failed: {e}")
61
- return {"error": str(e), "traceback": traceback.format_exc()}
 
44
  access_token = create_access_token(data={"sub": user["username"], "id": user["id"]})
45
  return {"access_token": access_token, "token_type": "bearer"}
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
RAG_FULL_APPLICATION_BACKEND/app/routers/ingest.py CHANGED
@@ -33,10 +33,11 @@ async def upload_file(
33
  strategy: str = Form("fixed"),
34
  user: dict = Depends(get_current_user)
35
  ):
 
36
  job_id = str(uuid.uuid4())
37
  temp_dir = Path("./data/uploads") / user["id"]
38
  temp_dir.mkdir(parents=True, exist_ok=True)
39
- file_path = temp_dir / file.filename
40
 
41
  with open(file_path, "wb") as f:
42
  f.write(await file.read())
@@ -45,7 +46,7 @@ async def upload_file(
45
  background_tasks.add_task(
46
  process_ingestion,
47
  str(file_path),
48
- file.filename,
49
  chunk_size,
50
  overlap,
51
  strategy,
 
33
  strategy: str = Form("fixed"),
34
  user: dict = Depends(get_current_user)
35
  ):
36
+ safe_filename = os.path.basename(file.filename)
37
  job_id = str(uuid.uuid4())
38
  temp_dir = Path("./data/uploads") / user["id"]
39
  temp_dir.mkdir(parents=True, exist_ok=True)
40
+ file_path = temp_dir / safe_filename
41
 
42
  with open(file_path, "wb") as f:
43
  f.write(await file.read())
 
46
  background_tasks.add_task(
47
  process_ingestion,
48
  str(file_path),
49
+ safe_filename,
50
  chunk_size,
51
  overlap,
52
  strategy,
RAG_FULL_APPLICATION_BACKEND/app/services/bm25_service.py CHANGED
@@ -1,6 +1,3 @@
1
- import pickle
2
- import os
3
- from pathlib import Path
4
  from typing import List, Dict, Any
5
  from rank_bm25 import BM25Okapi
6
  import logging
@@ -8,60 +5,48 @@ import logging
8
  logger = logging.getLogger(__name__)
9
 
10
  class BM25Service:
11
- def __init__(self, data_dir: str = "./data/bm25_indexes"):
12
- self.data_dir = Path(data_dir)
13
- self.data_dir.mkdir(parents=True, exist_ok=True)
14
-
15
- def _get_index_path(self, document_id: str) -> Path:
16
- return self.data_dir / f"{document_id}.pkl"
17
 
18
  def index_chunks(self, document_id: str, chunks: List[Dict[str, Any]]):
19
- """Build and save BM25 index for a document."""
20
- texts = [c["text"] for c in chunks]
21
- tokenized_corpus = [text.lower().split() for text in texts]
22
- bm25 = BM25Okapi(tokenized_corpus)
23
-
24
- # Save both the bm25 object and the chunk mapping
25
- with open(self._get_index_path(document_id), "wb") as f:
26
- pickle.dump({"bm25": bm25, "chunks": chunks}, f)
27
 
28
  def search(self, document_id: str, query: str, top_n: int = 10) -> List[Dict[str, Any]]:
29
- """Search using BM25."""
30
- path = self._get_index_path(document_id)
31
- if not path.exists():
32
- logger.warning(f"BM25 index not found for {document_id}. Attempting to rebuild from Supabase...")
33
- try:
34
- from .supabase_client import supabase_service
35
- result = supabase_service.client.table("chunks").select("*").eq("document_id", document_id).execute()
36
- chunks = result.data
37
- if chunks:
38
- logger.info(f"Rebuilding BM25 index for {document_id} with {len(chunks)} chunks.")
39
- self.index_chunks(document_id, chunks)
40
- else:
41
- logger.warning(f"No chunks found in Supabase for {document_id}. Cannot rebuild index.")
42
- return []
43
- except Exception as e:
44
- logger.error(f"Failed to rebuild BM25 index: {e}")
45
  return []
46
-
47
- with open(path, "rb") as f:
48
- data = pickle.load(f)
49
- bm25 = data["bm25"]
50
- chunks = data["chunks"]
51
-
52
- tokenized_query = query.lower().split()
53
- scores = bm25.get_scores(tokenized_query)
54
-
55
- # Add score to chunks
56
- results = []
57
- for i, score in enumerate(scores):
58
- if score > 0:
59
- chunk = chunks[i].copy()
60
- chunk["bm25_score"] = float(score)
61
- results.append(chunk)
62
-
63
- # Sort by score
64
- results.sort(key=lambda x: x["bm25_score"], reverse=True)
65
- return results[:top_n]
 
 
 
66
 
67
  bm25_service = BM25Service()
 
 
 
 
1
  from typing import List, Dict, Any
2
  from rank_bm25 import BM25Okapi
3
  import logging
 
5
  logger = logging.getLogger(__name__)
6
 
7
  class BM25Service:
8
+ def __init__(self):
9
+ # We no longer store pickle files to prevent RCE and memory bottlenecks.
10
+ pass
 
 
 
11
 
12
  def index_chunks(self, document_id: str, chunks: List[Dict[str, Any]]):
13
+ """Mock method for API compatibility. Chunks are now indexed dynamically on search."""
14
+ pass
15
+
16
+ def delete_document(self, document_id: str):
17
+ """Mock method for API compatibility. No files to delete."""
18
+ pass
 
 
19
 
20
  def search(self, document_id: str, query: str, top_n: int = 10) -> List[Dict[str, Any]]:
21
+ """Search using BM25 by dynamically fetching chunks from DB."""
22
+ try:
23
+ from .supabase_client import supabase_service
24
+ result = supabase_service.client.table("chunks").select("*").eq("document_id", document_id).execute()
25
+ chunks = result.data
26
+ if not chunks:
 
 
 
 
 
 
 
 
 
 
27
  return []
28
+
29
+ texts = [c["text"] for c in chunks]
30
+ tokenized_corpus = [text.lower().split() for text in texts]
31
+ bm25 = BM25Okapi(tokenized_corpus)
32
+
33
+ tokenized_query = query.lower().split()
34
+ scores = bm25.get_scores(tokenized_query)
35
+
36
+ # Add score to chunks
37
+ results = []
38
+ for i, score in enumerate(scores):
39
+ if score > 0:
40
+ chunk = chunks[i].copy()
41
+ chunk["bm25_score"] = float(score)
42
+ results.append(chunk)
43
+
44
+ # Sort by score
45
+ results.sort(key=lambda x: x["bm25_score"], reverse=True)
46
+ return results[:top_n]
47
+
48
+ except Exception as e:
49
+ logger.error(f"BM25 Search failed for {document_id}: {e}")
50
+ return []
51
 
52
  bm25_service = BM25Service()
RAG_FULL_APPLICATION_BACKEND/app/services/supabase_client.py CHANGED
@@ -109,7 +109,7 @@ class SupabaseService:
109
  """Delete document + chunks + vectors (CASCADE)"""
110
  try:
111
  # 1. Chunks (will cascade to vectors)
112
- self.client.table("chunks").delete().eq("document_id", document_id).execute()
113
  # 2. Document
114
  self.client.table("documents").delete().eq("id", document_id).eq("user_id", user_id).execute()
115
  except Exception as e:
 
109
  """Delete document + chunks + vectors (CASCADE)"""
110
  try:
111
  # 1. Chunks (will cascade to vectors)
112
+ self.client.table("chunks").delete().eq("document_id", document_id).eq("user_id", user_id).execute()
113
  # 2. Document
114
  self.client.table("documents").delete().eq("id", document_id).eq("user_id", user_id).execute()
115
  except Exception as e: