fix(security): tenant-scope FilePreviewApi text-extract endpoint (GHSA-2qwc-c2cc-2xwv) (#35797)

Signed-off-by: xr843 <137012659+xr843@users.noreply.github.com>
Co-authored-by: Ido Shani <ido@zafran.io>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
(cherry picked from commit 432a6412a3)
This commit is contained in:
Tim Ren 2026-05-15 00:13:04 +08:00 committed by Yunlu Wen
parent 9d903a5f79
commit 5b91f871b8
6 changed files with 24 additions and 14 deletions

View File

@ -38,6 +38,13 @@ The codebase is split into:
- Inject dependencies through constructors and preserve clean architecture boundaries.
- Handle errors with domain-specific exceptions at the correct layer.
## Cherry-Picking to LTS Branches
- Always use `git cherry-pick -x` when backporting commits from `main` to any `lts/*` branch.
- The `-x` flag appends `(cherry picked from commit <sha>)` to the commit message. CI validates this provenance line; commits without it will fail.
- When cherry-picking multiple commits, each must carry its own `(cherry picked from ...)` annotation.
- Run `git cherry-pick --continue` with `HUSKY=0` to skip pre-existing lint errors that are unrelated to the fix.
## Project Conventions
- Backend architecture adheres to DDD and Clean Architecture principles.

View File

@ -105,7 +105,8 @@ class FilePreviewApi(Resource):
@account_initialization_required
def get(self, file_id):
file_id = str(file_id)
text = FileService(db.engine).get_file_preview(file_id)
_, tenant_id = current_account_with_tenant()
text = FileService(db.engine).get_file_preview(file_id, tenant_id)
return {"content": text}

View File

@ -173,12 +173,14 @@ class FileService:
return upload_file
def get_file_preview(self, file_id: str):
def get_file_preview(self, file_id: str, tenant_id: str):
"""
Return a short text preview extracted from a document file.
"""
with self._session_maker(expire_on_commit=False) as session:
upload_file = session.query(UploadFile).where(UploadFile.id == file_id).first()
upload_file = session.scalar(
select(UploadFile).where(UploadFile.id == file_id, UploadFile.tenant_id == tenant_id).limit(1)
)
if not upload_file:
raise NotFound("File not found")

View File

@ -514,7 +514,7 @@ class TestFileService:
db_session_with_containers.commit()
result = FileService(engine).get_file_preview(file_id=upload_file.id)
result = FileService(engine).get_file_preview(file_id=upload_file.id, tenant_id=upload_file.tenant_id)
assert result == "extracted text content"
mock_external_service_dependencies["extract_processor"].load_from_upload_file.assert_called_once()
@ -529,7 +529,7 @@ class TestFileService:
non_existent_id = str(fake.uuid4())
with pytest.raises(NotFound, match="File not found"):
FileService(engine).get_file_preview(file_id=non_existent_id)
FileService(engine).get_file_preview(file_id=non_existent_id, tenant_id=str(fake.uuid4()))
def test_get_file_preview_unsupported_file_type(
self, db_session_with_containers: Session, engine, mock_external_service_dependencies
@ -549,7 +549,7 @@ class TestFileService:
db_session_with_containers.commit()
with pytest.raises(UnsupportedFileTypeError):
FileService(engine).get_file_preview(file_id=upload_file.id)
FileService(engine).get_file_preview(file_id=upload_file.id, tenant_id=upload_file.tenant_id)
def test_get_file_preview_text_truncation(
self, db_session_with_containers: Session, engine, mock_external_service_dependencies
@ -572,7 +572,7 @@ class TestFileService:
long_text = "x" * 5000 # Longer than PREVIEW_WORDS_LIMIT
mock_external_service_dependencies["extract_processor"].load_from_upload_file.return_value = long_text
result = FileService(engine).get_file_preview(file_id=upload_file.id)
result = FileService(engine).get_file_preview(file_id=upload_file.id, tenant_id=upload_file.tenant_id)
assert len(result) == 3000 # PREVIEW_WORDS_LIMIT
assert result == "x" * 3000

View File

@ -278,7 +278,7 @@ class TestFileApiPost:
class TestFilePreviewApi:
def test_get_preview(self, app, mock_file_service):
def test_get_preview(self, app, mock_account_context, mock_file_service):
api = FilePreviewApi()
get_method = unwrap(api.get)
mock_file_service.get_file_preview.return_value = "preview text"

View File

@ -215,29 +215,29 @@ class TestFileService:
upload_file = MagicMock(spec=UploadFile)
upload_file.id = "file_id"
upload_file.extension = "pdf"
mock_db_session.query().where().first.return_value = upload_file
mock_db_session.scalar.return_value = upload_file
with patch("services.file_service.ExtractProcessor.load_from_upload_file") as mock_extract:
mock_extract.return_value = "Extracted text content"
# Execute
result = file_service.get_file_preview("file_id")
result = file_service.get_file_preview("file_id", "tenant_id")
# Assert
assert result == "Extracted text content"
def test_get_file_preview_not_found(self, file_service, mock_db_session):
mock_db_session.query().where().first.return_value = None
mock_db_session.scalar.return_value = None
with pytest.raises(NotFound, match="File not found"):
file_service.get_file_preview("non_existent")
file_service.get_file_preview("non_existent", "tenant_id")
def test_get_file_preview_unsupported_type(self, file_service, mock_db_session):
upload_file = MagicMock(spec=UploadFile)
upload_file.id = "file_id"
upload_file.extension = "exe"
mock_db_session.query().where().first.return_value = upload_file
mock_db_session.scalar.return_value = upload_file
with pytest.raises(UnsupportedFileTypeError):
file_service.get_file_preview("file_id")
file_service.get_file_preview("file_id", "tenant_id")
def test_get_image_preview_success(self, file_service, mock_db_session):
# Setup