Files
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

198 lines
5.5 KiB
TypeScript

import { MongoClient } from 'mongodb'
import {
createPinnedLookup,
validateDatabaseHost,
} from '@/lib/core/security/input-validation.server'
import type { MongoDBCollectionInfo, MongoDBConnectionConfig } from '@/tools/mongodb/types'
export async function createMongoDBConnection(config: MongoDBConnectionConfig) {
const hostValidation = await validateDatabaseHost(config.host, 'host')
if (!hostValidation.isValid) {
throw new Error(hostValidation.error)
}
const credentials =
config.username && config.password
? `${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@`
: ''
const queryParams = new URLSearchParams()
if (config.authSource) {
queryParams.append('authSource', config.authSource)
}
if (config.ssl === 'required') {
queryParams.append('ssl', 'true')
}
const queryString = queryParams.toString()
const uri = `mongodb://${credentials}${config.host}:${config.port}/${config.database}${queryString ? `?${queryString}` : ''}`
const client = new MongoClient(uri, {
connectTimeoutMS: 10000,
socketTimeoutMS: 10000,
maxPoolSize: 1,
lookup: createPinnedLookup(hostValidation.resolvedIP ?? config.host),
})
await client.connect()
return client
}
/**
* Recursively checks an object for dangerous MongoDB operators
* @param obj - The object to check
* @param dangerousOperators - Array of operator names to block
* @returns true if a dangerous operator is found
*/
function containsDangerousOperator(obj: unknown, dangerousOperators: string[]): boolean {
if (typeof obj !== 'object' || obj === null) return false
for (const key of Object.keys(obj as Record<string, unknown>)) {
if (dangerousOperators.includes(key)) return true
if (
typeof (obj as Record<string, unknown>)[key] === 'object' &&
containsDangerousOperator((obj as Record<string, unknown>)[key], dangerousOperators)
) {
return true
}
}
return false
}
export function validateFilter(filter: string): { isValid: boolean; error?: string } {
try {
const parsed = JSON.parse(filter)
const dangerousOperators = [
'$where', // Executes arbitrary JavaScript
'$regex', // Can cause ReDoS attacks
'$expr', // Expression evaluation
'$function', // Custom JavaScript functions
'$accumulator', // Custom JavaScript accumulators
'$let', // Variable definitions that could be exploited
]
if (containsDangerousOperator(parsed, dangerousOperators)) {
return {
isValid: false,
error: 'Filter contains potentially dangerous operators',
}
}
return { isValid: true }
} catch (error) {
return {
isValid: false,
error: 'Invalid JSON format in filter',
}
}
}
export function validatePipeline(pipeline: string): { isValid: boolean; error?: string } {
try {
const parsed = JSON.parse(pipeline)
if (!Array.isArray(parsed)) {
return {
isValid: false,
error: 'Pipeline must be an array',
}
}
const dangerousOperators = [
'$where', // Executes arbitrary JavaScript
'$function', // Custom JavaScript functions
'$accumulator', // Custom JavaScript accumulators
'$let', // Variable definitions that could be exploited
'$merge', // Writes to external collections
'$out', // Writes to external collections
'$currentOp', // Exposes system operation info
'$listSessions', // Exposes session info
'$listLocalSessions', // Exposes local session info
]
for (const stage of parsed) {
if (containsDangerousOperator(stage, dangerousOperators)) {
return {
isValid: false,
error: 'Pipeline contains potentially dangerous operators',
}
}
}
return { isValid: true }
} catch (error) {
return {
isValid: false,
error: 'Invalid JSON format in pipeline',
}
}
}
export function sanitizeCollectionName(name: string): string {
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
throw new Error(
'Invalid collection name. Must start with letter or underscore and contain only letters, numbers, and underscores.'
)
}
return name
}
/**
* Introspect MongoDB to get databases, collections, and indexes
*/
export async function executeIntrospect(
client: MongoClient,
database?: string
): Promise<{
message: string
databases: string[]
collections: MongoDBCollectionInfo[]
}> {
const databases: string[] = []
const collections: MongoDBCollectionInfo[] = []
if (database) {
databases.push(database)
const db = client.db(database)
const collectionList = await db.listCollections().toArray()
for (const collInfo of collectionList) {
const coll = db.collection(collInfo.name)
const indexes = await coll.indexes()
const documentCount = await coll.estimatedDocumentCount()
collections.push({
name: collInfo.name,
type: collInfo.type || 'collection',
documentCount,
indexes: indexes.map((idx) => ({
name: idx.name || '',
key: idx.key as Record<string, number>,
unique: idx.unique || false,
sparse: idx.sparse,
})),
})
}
} else {
const admin = client.db().admin()
const dbList = await admin.listDatabases()
for (const dbInfo of dbList.databases) {
databases.push(dbInfo.name)
}
}
const message = database
? `Found ${collections.length} collections in database '${database}'`
: `Found ${databases.length} databases`
return {
message,
databases,
collections,
}
}