This document covers the security architecture, practices, and considerations for the VisionFlow Vircadia integration.
All SQL queries sent from the client to the Vircadia World Server use parameterized placeholders ($1, $2, ...) to prevent SQL injection. The QueryOptions interface enforces this pattern:
interface QueryOptions {
query: string; // SQL with $1, $2, ... placeholders
parameters?: unknown[]; // Bound parameter values
timeoutMs?: number;
}The following services use fully parameterized queries:
| Service | Operations | Status |
|---|---|---|
| VircadiaClientCore | All query() calls |
Parameterized |
| ThreeJSAvatarRenderer | Avatar INSERT, UPDATE, SELECT | Parameterized |
| EntitySyncManager | Entity INSERT, UPDATE, DELETE, SELECT | Parameterized |
| GraphEntityMapper | All generated SQL | Parameterized |
Example -- avatar position broadcast:
const query = `
UPDATE entity.entities
SET meta__data = jsonb_set(
jsonb_set(
jsonb_set(meta__data, '{position}', $1::jsonb),
'{rotation}', $2::jsonb
),
'{timestamp}', $3::text::jsonb
)
WHERE general__entity_name = $4
`;
await client.Utilities.Connection.query({
query,
parameters: [
JSON.stringify({ x: pos.x, y: pos.y, z: pos.z }),
JSON.stringify({ x: rot.x, y: rot.y, z: rot.z, w: rot.w }),
String(Date.now()),
`avatar_${agentId}`
]
});The following services contain string-interpolated SQL. These queries construct entity names or JSON payloads via template literals rather than bound parameters:
| Service | Methods | Risk |
|---|---|---|
| SpatialAudioManager | sendOffer, sendAnswer, sendICECandidate, handleSignalingMessages |
Entity names and JSON payloads interpolated |
| NetworkOptimizer | flushBatch (non-compressed path) |
Position values and entity names interpolated |
| Quest3Optimizer | broadcastHandData, broadcastControllerState |
JSON payloads and entity names interpolated |
These methods interpolate local agent IDs and serialized JSON into SQL strings. While the values originate from trusted client-side state (not user input), this pattern should be migrated to parameterized queries for defense-in-depth.
Authentication happens at WebSocket connection time. The client passes credentials as URL query parameters:
wss://host:3020/world/ws?token=<auth_token>&provider=<auth_provider>
The Vircadia World Server validates the token against the configured auth provider and returns a SESSION_INFO_RESPONSE with:
agentId-- unique identifier for the connected agentsessionId-- session identifier for the connection
| Provider | Description |
|---|---|
system |
Internal token-based authentication |
nostr |
Nostr identity-based authentication (NIP-07) |
- Sessions have a configurable timeout (default: 86400 seconds / 24 hours)
- The client maintains a heartbeat every 30 seconds using a
SELECT 1 as heartbeatquery - If the heartbeat fails or the WebSocket closes, the client enters reconnection mode
- Maximum reconnection attempts and delay are configurable via
ClientCoreConfig
- Auth tokens are transmitted over the WebSocket URL, which means they appear in server access logs and browser history
- For production deployments, use WSS (WebSocket Secure) to encrypt the connection
- Tokens should have a limited lifetime and be rotated regularly
- The
VIRCADIA_JWT_SECRETenvironment variable must be changed from its default value before production deployment
Spatial audio uses WebRTC peer connections. The default configuration uses Google's public STUN servers:
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]For production deployments behind NATs or firewalls, configure a TURN server:
iceServers: [
{ urls: 'stun:stun.example.com:3478' },
{
urls: 'turn:turn.example.com:3478',
username: 'user',
credential: 'password'
}
]WebRTC mandates DTLS (Datagram Transport Layer Security) for all peer connections. Audio streams between peers are encrypted by default. This is handled by the browser's WebRTC implementation and requires no additional configuration.
WebRTC signaling (offer/answer/ICE candidate exchange) is performed through the Vircadia entity store. Signaling messages are stored as entities with names like:
webrtc_offer_{fromAgent}_{toAgent}webrtc_answer_{fromAgent}_{toAgent}webrtc_ice_{fromAgent}_{toAgent}_{timestamp}
These entities are readable by any connected client querying the entity store. In a multi-tenant deployment, signaling entities should be scoped to prevent cross-world information leakage.
| Boundary | Validation |
|---|---|
| WebSocket messages | JSON.parse with try/catch; malformed messages are logged and discarded |
| Binary protocol | Header size validation; payload length verification; protocol version check |
| Query responses | Request ID matching; timeout enforcement per query |
| Remote avatar data | Position and rotation values parsed from entity metadata with null checks |
| Hand tracking joints | Joint array bounds checking before mesh updates |
| Feature flags | rolloutPercentage clamped to 0-100 range; localStorage parse errors caught |
The Vircadia World Server enforces:
- SQL query parsing and validation before execution
- Entity name uniqueness constraints
- Sync group access control
- Maximum entities per user (configurable, default: 1000)
- Connection authentication via token verification
The BinaryWebSocketProtocol class performs defensive validation on all incoming binary messages:
- Header size check -- buffer must be at least
MESSAGE_HEADER_SIZE(4 bytes) - Protocol version check -- only V2 and V3 are accepted; unsupported versions are rejected
- Payload length verification -- declared payload length must match actual buffer size
- Per-record size validation -- position updates must be exact multiples of
AGENT_POSITION_SIZE(21 bytes) - Truncation detection -- partial records at the end of a buffer are logged and skipped
The SQL-over-WebSocket pattern means the client sends raw SQL queries to the server. While the server validates and executes these queries, any connected client can submit arbitrary queries within its permission scope. The server-side query validator and PostgreSQL role permissions are the primary defense.
WebRTC signaling data (SDP offers/answers, ICE candidates) is stored in the same entity table as application data. Any connected client can read signaling entities. For sensitive deployments, signaling should use a dedicated channel or be encrypted at the application layer.
Feature flags are persisted to localStorage, which is accessible to any JavaScript running on the same origin. Feature flags should not be used as a security boundary -- they control UX behavior, not access control.
The Docker Compose configuration ships with default credentials that must be changed before production deployment:
| Credential | Default Value | Environment Variable |
|---|---|---|
| PostgreSQL password | visionflow_secure |
POSTGRES_PASSWORD |
| JWT secret | change_this_in_production |
VIRCADIA_JWT_SECRET |
The SpatialAudioManager requests microphone access via navigator.mediaDevices.getUserMedia(). The browser will prompt the user for permission. Audio streams are only shared with established WebRTC peers and are encrypted via DTLS.
- Migrate all SQL queries to parameterized form -- Prioritize SpatialAudioManager, NetworkOptimizer, and Quest3Optimizer
- Deploy with WSS -- Always use TLS-encrypted WebSocket connections in production
- Configure a TURN server -- Required for reliable connectivity behind NATs and firewalls
- Change all default credentials -- PostgreSQL password, JWT secret, and any API keys
- Scope signaling entities -- Add access control to prevent cross-world signaling leakage
- Rate-limit query submissions -- Server-side rate limiting on the SQL-over-WebSocket interface
- Audit entity permissions -- Ensure PostgreSQL roles restrict what queries each auth provider can execute