Security
Docs / Core / Security
Security considerations and best practices for ZenECS.
Overview
ZenECS is designed with security in mind, but proper configuration and usage patterns are essential for secure applications. This guide covers security considerations for serialization, input validation, threading, and access control.
Key Security Principles:
- Defense in Depth: Multiple layers of security
- Principle of Least Privilege: Restrict access to minimum necessary
- Input Validation: Validate all external inputs
- Secure Defaults: Safe defaults with explicit opt-in for risky operations
- Audit Trail: Log security-relevant events
Serialization Security
Serialization is a common attack vector. ZenECS provides mechanisms to secure serialization operations.
Untrusted Data
Never deserialize untrusted data without validation.
// ❌ DANGEROUS: Loading untrusted snapshot
using (var stream = File.OpenRead("user_uploaded_save.dat"))
{
world.LoadSnapshot(stream); // Risk: Type confusion, code injection
}
// ✅ SAFE: Validate source and use allowlist
if (!IsTrustedSource(filePath))
throw new SecurityException("Untrusted source");
// Use allowlist of allowed component types
var allowedTypes = new HashSet<Type> { typeof(Position), typeof(Health) };
world.LoadSnapshot(stream, allowedTypes);
Type Safety
Validate component types during deserialization:
public class SecureSnapshotLoader
{
private readonly HashSet<string> _allowedStableIds;
public SecureSnapshotLoader()
{
// Only allow known, safe component types
_allowedStableIds = new HashSet<string>
{
ComponentRegistry.GetId<Position>(),
ComponentRegistry.GetId<Health>(),
ComponentRegistry.GetId<Velocity>()
};
}
public void LoadSnapshot(IWorld world, Stream stream)
{
// Validate types before loading
var snapshotTypes = ReadSnapshotTypes(stream);
foreach (var typeId in snapshotTypes)
{
if (!_allowedStableIds.Contains(typeId))
{
throw new SecurityException($"Disallowed component type: {typeId}");
}
}
// Safe to load after validation
world.LoadSnapshot(stream);
}
}
Custom Formatters
Implement secure formatters for sensitive data:
public class SecureHealthFormatter : IComponentFormatter
{
public Type ComponentType => typeof(Health);
public void Write(object boxed, ISnapshotBackend backend)
{
var health = (Health)boxed;
// Validate before serialization
if (health.Current < 0 || health.Current > health.Max)
throw new ArgumentException("Invalid health value");
backend.WriteFloat(health.Current);
backend.WriteFloat(health.Max);
}
public object Read(ISnapshotBackend backend)
{
var current = backend.ReadFloat();
var max = backend.ReadFloat();
// Validate after deserialization
if (current < 0 || current > max || max <= 0)
throw new SecurityException("Invalid health data in snapshot");
return new Health { Current = current, Max = max };
}
}
Snapshot Integrity
Verify snapshot integrity:
public class SecureSnapshotManager
{
public void SaveSnapshot(IWorld world, Stream stream, byte[] secretKey)
{
using (var memoryStream = new MemoryStream())
{
world.SaveSnapshot(memoryStream);
// Compute hash
var hash = ComputeHMAC(memoryStream.ToArray(), secretKey);
// Write hash before snapshot
stream.Write(hash, 0, hash.Length);
memoryStream.Position = 0;
memoryStream.CopyTo(stream);
}
}
public void LoadSnapshot(IWorld world, Stream stream, byte[] secretKey)
{
// Read and verify hash
var hash = new byte[32];
stream.Read(hash, 0, hash.Length);
var snapshotData = new byte[stream.Length - hash.Length];
stream.Read(snapshotData, 0, snapshotData.Length);
var computedHash = ComputeHMAC(snapshotData, secretKey);
if (!ConstantTimeEquals(hash, computedHash))
{
throw new SecurityException("Snapshot integrity check failed");
}
// Safe to load
using (var snapshotStream = new MemoryStream(snapshotData))
{
world.LoadSnapshot(snapshotStream);
}
}
private byte[] ComputeHMAC(byte[] data, byte[] key)
{
using (var hmac = new System.Security.Cryptography.HMACSHA256(key))
{
return hmac.ComputeHash(data);
}
}
private bool ConstantTimeEquals(byte[] a, byte[] b)
{
if (a.Length != b.Length) return false;
int result = 0;
for (int i = 0; i < a.Length; i++)
result |= a[i] ^ b[i];
return result == 0;
}
}
Input Validation
All external inputs must be validated before use.
Component Value Validation
Use validators to enforce data constraints:
// Validate component values
world.AddValidator<Health>(health =>
{
// Enforce bounds
if (health.Current < 0 || health.Current > health.Max)
return false;
// Prevent NaN and Infinity
if (float.IsNaN(health.Current) || float.IsInfinity(health.Current))
return false;
if (float.IsNaN(health.Max) || float.IsInfinity(health.Max))
return false;
return true;
});
world.AddValidator<Position>(pos =>
{
// Prevent extreme values that could cause overflow
if (Math.Abs(pos.X) > 1000000 || Math.Abs(pos.Y) > 1000000)
return false;
// Prevent NaN and Infinity
if (float.IsNaN(pos.X) || float.IsNaN(pos.Y))
return false;
if (float.IsInfinity(pos.X) || float.IsInfinity(pos.Y))
return false;
return true;
});
Entity ID Validation
Validate entity IDs from external sources:
public bool IsValidEntityId(ulong entityHandle)
{
var (id, gen) = Entity.Unpack(entityHandle);
// Check ID range
if (id <= 0 || id > int.MaxValue)
return false;
// Check generation
if (gen < 0 || gen > int.MaxValue)
return false;
// Verify entity exists and generation matches
if (!world.IsAlive(Entity.Pack(id, gen)))
return false;
return world.GenerationOf(id) == gen;
}
String Input Validation
Sanitize string inputs:
public string SanitizeString(string input, int maxLength = 256)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
// Limit length
if (input.Length > maxLength)
input = input.Substring(0, maxLength);
// Remove control characters
input = new string(input.Where(c => !char.IsControl(c)).ToArray());
return input;
}
Access Control
Control who can modify components using permission hooks.
Write Permissions
Implement role-based access control:
public class SecuritySystem
{
public void SetupPermissions(IWorld world)
{
// Only server can modify authoritative components
world.AddWritePermission((entity, componentType) =>
{
if (componentType == typeof(ServerAuthoritative))
{
return IsServer();
}
return true;
});
// Only admins can modify admin components
world.AddWritePermission((entity, componentType) =>
{
if (componentType == typeof(AdminComponent))
{
return world.HasComponent<Admin>(entity) &&
IsCurrentUserAdmin();
}
return true;
});
// Locked entities cannot be modified
world.AddWritePermission((entity, componentType) =>
{
if (world.HasComponent<Locked>(entity))
{
return HasUnlockPermission(entity);
}
return true;
});
}
private bool IsServer() => /* ... */;
private bool IsCurrentUserAdmin() => /* ... */;
private bool HasUnlockPermission(Entity entity) => /* ... */;
}
Read Permissions
Control read access for sensitive data:
public class SecureQuerySystem
{
public IEnumerable<(Entity, Health)> QueryHealthSecurely(IWorld world)
{
foreach (var (entity, health) in world.Query<Health>())
{
// Check read permission
if (HasReadPermission(entity, typeof(Health)))
{
yield return (entity, health);
}
}
}
private bool HasReadPermission(Entity entity, Type componentType)
{
// Implement read permission logic
// For example: only owner or admin can read sensitive health
return true; // Implement your logic
}
}
Threading Safety
ZenECS provides thread-safe operations, but proper usage is required.
Concurrent Access
Kernel and World operations are thread-safe for reads:
// ✅ SAFE: Concurrent reads
Parallel.ForEach(entities, entity =>
{
if (world.IsAlive(entity))
{
var pos = world.Get<Position>(entity);
// Process position
}
});
// ❌ UNSAFE: Concurrent writes without synchronization
Parallel.ForEach(entities, entity =>
{
using (var cmd = world.BeginWrite())
{
cmd.ReplaceComponent(entity, new Position { X = 1, Y = 1 });
}
// Risk: Race conditions, inconsistent state
});
// ✅ SAFE: Synchronized writes
lock (writeLock)
{
using (var cmd = world.BeginWrite())
{
foreach (var entity in entities)
{
cmd.ReplaceComponent(entity, new Position { X = 1, Y = 1 });
}
}
}
Message Bus Thread Safety
Message bus is thread-safe for publishing:
// ✅ SAFE: Concurrent message publishing
Parallel.ForEach(messages, msg =>
{
world.Publish(msg); // Thread-safe
});
// ⚠️ CAUTION: Subscriber execution order is not guaranteed
world.Subscribe<DamageMessage>(msg =>
{
// This may execute concurrently if multiple threads publish
// Ensure subscriber is thread-safe
ProcessDamage(msg);
});
Command Buffer Thread Safety
Command buffers are not thread-safe:
// ❌ UNSAFE: Sharing command buffer across threads
var cmd = world.BeginWrite();
Parallel.ForEach(entities, entity =>
{
cmd.AddComponent(entity, new Position()); // Race condition!
});
// ✅ SAFE: One command buffer per thread
Parallel.ForEach(entities, entity =>
{
using (var cmd = world.BeginWrite())
{
cmd.AddComponent(entity, new Position());
}
});
Secure Defaults
Configure secure defaults for production.
Write Failure Policy
Use appropriate policy for environment:
#if DEVELOPMENT
// Development: Strict error checking
EcsRuntimeOptions.WritePolicy = EcsRuntimeOptions.WriteFailurePolicy.Throw;
#else
// Production: Log and continue (prevent DoS from exceptions)
EcsRuntimeOptions.WritePolicy = EcsRuntimeOptions.WriteFailurePolicy.Log;
#endif
Error Reporting
Configure secure error reporting:
// Don't expose sensitive information in error messages
EcsRuntimeOptions.OnUnhandledError = (ex) =>
{
// Log full details internally
InternalLogger.LogError(ex);
// Send sanitized version to external service
var sanitized = new Exception(SanitizeErrorMessage(ex.Message));
ErrorTrackingService.CaptureException(sanitized);
};
private string SanitizeErrorMessage(string message)
{
// Remove sensitive data (paths, tokens, etc.)
return message
.Replace(Environment.UserName, "[USER]")
.Replace(Environment.MachineName, "[MACHINE]");
}
Logging Security
Avoid logging sensitive data:
public class SecureLogger : IEcsLogger
{
public void Info(string message)
{
// Sanitize before logging
var sanitized = SanitizeLogMessage(message);
File.AppendAllText("log.txt", $"[INFO] {sanitized}\n");
}
public void Warn(string message)
{
var sanitized = SanitizeLogMessage(message);
File.AppendAllText("log.txt", $"[WARN] {sanitized}\n");
}
public void Error(string message)
{
var sanitized = SanitizeLogMessage(message);
File.AppendAllText("log.txt", $"[ERROR] {sanitized}\n");
}
private string SanitizeLogMessage(string message)
{
// Remove sensitive patterns
return System.Text.RegularExpressions.Regex.Replace(
message,
@"(password|token|secret|key)\s*[:=]\s*\S+",
"$1: [REDACTED]",
System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
}
}
Network Security
For networked applications, additional security measures are required.
State Synchronization
Validate server state before applying:
public class SecureStateSync
{
public void ApplyServerState(IWorld world, Stream serverState, byte[] serverSignature)
{
// Verify server signature
if (!VerifySignature(serverState, serverSignature))
{
throw new SecurityException("Invalid server signature");
}
// Validate state structure
var stateTypes = ReadStateTypes(serverState);
if (!IsValidStateStructure(stateTypes))
{
throw new SecurityException("Invalid state structure");
}
// Apply state
world.LoadSnapshot(serverState);
}
private bool VerifySignature(Stream data, byte[] signature)
{
// Implement signature verification
return true; // Implement your logic
}
private bool IsValidStateStructure(IEnumerable<string> types)
{
var allowedTypes = GetAllowedComponentTypes();
return types.All(t => allowedTypes.Contains(t));
}
}
Client-Side Validation
Never trust client input:
// ❌ DANGEROUS: Trusting client input
public void ProcessClientInput(Entity entity, Position newPos)
{
using (var cmd = world.BeginWrite())
{
cmd.ReplaceComponent(entity, newPos); // Client can send any value!
}
}
// ✅ SAFE: Validate and sanitize client input
public void ProcessClientInput(Entity entity, Position newPos, float serverPosX, float serverPosY)
{
// Validate movement speed
var distance = Math.Sqrt(
Math.Pow(newPos.X - serverPosX, 2) +
Math.Pow(newPos.Y - serverPosY, 2)
);
const float maxSpeed = 10f; // units per frame
if (distance > maxSpeed)
{
// Clamp to maximum speed
var direction = Math.Atan2(newPos.Y - serverPosY, newPos.X - serverPosX);
newPos = new Position
{
X = serverPosX + (float)(Math.Cos(direction) * maxSpeed),
Y = serverPosY + (float)(Math.Sin(direction) * maxSpeed)
};
}
// Validate bounds
if (newPos.X < -1000 || newPos.X > 1000 ||
newPos.Y < -1000 || newPos.Y > 1000)
{
throw new SecurityException("Position out of bounds");
}
using (var cmd = world.BeginWrite())
{
cmd.ReplaceComponent(entity, newPos);
}
}
Best Practices
1. Validate All Inputs
Always validate inputs from external sources:
public void ProcessExternalInput<T>(Entity entity, T component) where T : struct
{
// Validate entity
if (!world.IsAlive(entity))
throw new ArgumentException("Invalid entity");
// Validate component (use validators)
// Validators are automatically checked
// Process
using (var cmd = world.BeginWrite())
{
cmd.ReplaceComponent(entity, component);
}
}
2. Use Allowlists
Prefer allowlists over blocklists:
// ✅ GOOD: Allowlist
private readonly HashSet<Type> _allowedComponents = new()
{
typeof(Position),
typeof(Velocity),
typeof(Health)
};
public bool CanAddComponent(Type componentType)
{
return _allowedComponents.Contains(componentType);
}
// ❌ BAD: Blocklist (easy to miss new threats)
private readonly HashSet<Type> _blockedComponents = new()
{
typeof(AdminComponent)
};
3. Principle of Least Privilege
Grant minimum necessary permissions:
// Grant specific permissions per entity/component
world.AddWritePermission((entity, componentType) =>
{
// Check specific permission for this entity/component pair
return HasSpecificPermission(entity, componentType);
});
4. Audit Security Events
Log security-relevant events:
public class SecurityAudit
{
public void LogWriteAttempt(Entity entity, Type componentType, bool allowed)
{
if (!allowed)
{
AuditLog.LogWarning(
$"Write denied: Entity={entity.Id}, " +
$"Component={componentType.Name}, " +
$"User={GetCurrentUser()}, " +
$"Time={DateTime.UtcNow}"
);
}
}
public void LogValidationFailure(Entity entity, Type componentType, string reason)
{
AuditLog.LogError(
$"Validation failed: Entity={entity.Id}, " +
$"Component={componentType.Name}, " +
$"Reason={reason}, " +
$"Time={DateTime.UtcNow}"
);
}
}
5. Secure Configuration
Store sensitive configuration securely:
// ❌ BAD: Hardcoded secrets
const string API_KEY = "secret123";
// ✅ GOOD: Use secure configuration
var apiKey = Environment.GetEnvironmentVariable("API_KEY")
?? throw new InvalidOperationException("API_KEY not set");
// Or use secure configuration service
var apiKey = SecureConfig.GetSecret("api_key");
Security Checklist
Before deploying to production:
- [ ] All external inputs are validated
- [ ] Serialization uses allowlists for component types
- [ ] Snapshot integrity is verified (HMAC/signature)
- [ ] Write permissions are properly configured
- [ ] Validators enforce all data constraints
- [ ] Error messages don't expose sensitive information
- [ ] Logging doesn't include sensitive data
- [ ] Thread safety is properly handled
- [ ] Network state is validated before applying
- [ ] Client inputs are validated and sanitized
- [ ] Security events are audited
- [ ] Secrets are stored securely (not hardcoded)
See Also
- Error Handling - Error handling and reporting
- Write Hooks & Validators - Access control
- Serialization - Serialization details
- Performance - Performance considerations