diff --git a/packages/backend/native/src/backend_runtime/blob_complete.rs b/packages/backend/native/src/backend_runtime/blob_complete.rs index 140561b19d..4c8c0edbf3 100644 --- a/packages/backend/native/src/backend_runtime/blob_complete.rs +++ b/packages/backend/native/src/backend_runtime/blob_complete.rs @@ -10,6 +10,8 @@ use sha2::{Digest, Sha256}; use super::{BackendRuntime, error::napi_error, types::RuntimeBlobCompleteResult}; +const MAX_BLOB_SIZE: i64 = i32::MAX as i64; + fn object_missing_error(err: &napi::Error) -> bool { let message = err.to_string(); message.contains("NoSuchKey") || message.contains("NotFound") || message.contains("not found") @@ -110,6 +112,11 @@ async fn upsert_completed_blob( mime: &str, size: i64, ) -> Result<()> { + if !(0..=MAX_BLOB_SIZE).contains(&size) { + return Err(napi_error("BlobComplete size exceeds limit")); + } + let size = i32::try_from(size).map_err(|_| napi_error("BlobComplete size exceeds limit"))?; + sqlx::query( r#" INSERT INTO blobs (workspace_id, key, mime, size, status, upload_id) @@ -125,7 +132,7 @@ async fn upsert_completed_blob( .bind(workspace_id) .bind(key) .bind(mime) - .bind(size as i32) + .bind(size) .execute(&runtime.pool().await?) .await .map_err(|err| napi_error(format!("BlobComplete upsert metadata failed: {err}")))?; @@ -143,6 +150,10 @@ impl BackendRuntime { expected_size: i64, expected_mime: String, ) -> Result { + if !(0..=MAX_BLOB_SIZE).contains(&expected_size) { + return Ok(blob_complete_failure("size_too_large")); + } + let object_key = format!("{workspace_id}/{key}"); let object = match self.object_storage_get(object_key.clone()).await { Ok(Some(object)) => object, @@ -151,6 +162,14 @@ impl BackendRuntime { Err(err) => return Err(err), }; + if !(0..=MAX_BLOB_SIZE).contains(&object.metadata.content_length) { + match self.object_storage_delete(object_key).await { + Ok(()) => {} + Err(err) if object_missing_error(&err) => {} + Err(err) => return Err(err), + } + return Ok(blob_complete_failure("size_too_large")); + } if object.metadata.content_length != expected_size { return Ok(blob_complete_failure("size_mismatch")); } @@ -194,6 +213,10 @@ impl BackendRuntime { expected_size: i64, expected_mime: String, ) -> Result { + if !(0..=MAX_BLOB_SIZE).contains(&expected_size) { + return Ok(blob_complete_failure("size_too_large")); + } + let storage_key = format!("{workspace_id}/{key}"); let path = fs_object_path(&root, &bucket, &storage_key)?; let metadata = match read_fs_metadata(&path)? { @@ -201,6 +224,11 @@ impl BackendRuntime { None => return Ok(blob_complete_failure("not_found")), }; + if !(0..=MAX_BLOB_SIZE).contains(&metadata.content_length) { + let _ = fs::remove_file(&path); + let _ = fs::remove_file(PathBuf::from(format!("{}.metadata.json", path.display()))); + return Ok(blob_complete_failure("size_too_large")); + } if metadata.content_length != expected_size { return Ok(blob_complete_failure("size_mismatch")); } diff --git a/packages/backend/native/src/backend_runtime/tests.rs b/packages/backend/native/src/backend_runtime/tests.rs index 86a692b95b..a734259ab9 100644 --- a/packages/backend/native/src/backend_runtime/tests.rs +++ b/packages/backend/native/src/backend_runtime/tests.rs @@ -1,7 +1,7 @@ -use super::{runtime_state::*, *}; - use anyhow::{Context, Result as AnyResult, anyhow}; +use super::{runtime_state::*, *}; + static PG_TEST_LOCK: std::sync::OnceLock> = std::sync::OnceLock::new(); const TEST_VERIFICATION_TOKEN_TYPE: i32 = 99_999; diff --git a/packages/backend/server/src/native.ts b/packages/backend/server/src/native.ts index 9a012f62d5..8675ebbbf9 100644 --- a/packages/backend/server/src/native.ts +++ b/packages/backend/server/src/native.ts @@ -2,6 +2,7 @@ import serverNativeModule, { type ActionEvent as NativeActionEventContract, type ActionRuntimeInput as NativeActionRuntimeInputContract, type AssertSafeUrlRequest, + type BackendRuntimeHealth, type BuiltInPromptRenderContract, type BuiltInPromptSessionContract, type BuiltInPromptSpec, @@ -45,6 +46,22 @@ import serverNativeModule, { type RequestedModelMatchResponse, type ResolvedEntitlement, type ResolveEntitlementInput, + type RuntimeBlobCleanupResult, + type RuntimeBlobCompleteResult, + type RuntimeByokLocalLeaseRecord, + type RuntimeDocCompactionResult, + type RuntimeMagicLinkOtpConsumeResult, + type RuntimeMultipartUploadInit, + type RuntimeMultipartUploadPart, + type RuntimeObjectGetResult, + type RuntimeObjectListEntry, + type RuntimeObjectMetadata, + type RuntimeObjectStorageHealth, + type RuntimeObjectStoragePutOptions, + type RuntimePresignedObjectRequest, + type RuntimeVerificationTokenRecord, + type RuntimeWorkspaceInviteLinkRecord, + type RuntimeWorkspaceStatsDailyRecalibrationResult, type SafeFetchRequest, type SafeFetchResponse, type Tokenizer, @@ -52,6 +69,7 @@ import serverNativeModule, { export type { AssertSafeUrlRequest, + BackendRuntimeHealth, CapabilityAttachmentContract, CapabilityModelCapability, CommandResponse, @@ -73,6 +91,22 @@ export type { RemoteMimeTypeRequest, ResolvedEntitlement, ResolveEntitlementInput, + RuntimeBlobCleanupResult, + RuntimeBlobCompleteResult, + RuntimeByokLocalLeaseRecord, + RuntimeDocCompactionResult, + RuntimeMagicLinkOtpConsumeResult, + RuntimeMultipartUploadInit, + RuntimeMultipartUploadPart, + RuntimeObjectGetResult, + RuntimeObjectListEntry, + RuntimeObjectMetadata, + RuntimeObjectStorageHealth, + RuntimeObjectStoragePutOptions, + RuntimePresignedObjectRequest, + RuntimeVerificationTokenRecord, + RuntimeWorkspaceInviteLinkRecord, + RuntimeWorkspaceStatsDailyRecalibrationResult, SafeFetchRequest, SafeFetchResponse, }; @@ -180,6 +214,7 @@ export const readAllDocIdsFromRootDoc = export const AFFINE_PRO_PUBLIC_KEY = serverNativeModule.AFFINE_PRO_PUBLIC_KEY; export const AFFINE_PRO_LICENSE_AES_KEY = serverNativeModule.AFFINE_PRO_LICENSE_AES_KEY; +export const BackendRuntime = serverNativeModule.BackendRuntime; export type PermissionWorkspaceRole = 'external' | 'member' | 'admin' | 'owner'; export type PermissionDocRole =