Security Guide
This comprehensive guide covers security best practices for developing and deploying QQ Guild bots with BotRS. Security should be a primary consideration throughout the development lifecycle.
Overview
Bot security encompasses several critical areas:
- Credential Management - Protecting authentication tokens and secrets
- Input Validation - Preventing injection attacks and malformed data
- Permission Management - Implementing proper access controls
- Data Protection - Securing user data and communications
- Infrastructure Security - Hardening deployment environments
Credential Security
Token Management
Never hardcode credentials in source code:
// ❌ BAD: Hardcoded credentials
let token = Token::new("123456789", "my_secret_key");
// ✅ GOOD: Environment variables
let app_id = std::env::var("QQ_BOT_APP_ID")
.expect("QQ_BOT_APP_ID environment variable not set");
let secret = std::env::var("QQ_BOT_SECRET")
.expect("QQ_BOT_SECRET environment variable not set");
let token = Token::new(app_id, secret);
Secure Token Storage
Use secure storage mechanisms for production:
use std::fs;
use std::path::Path;
// Secure configuration loading
pub struct SecureConfig {
app_id: String,
secret: String,
}
impl SecureConfig {
pub fn load_from_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
// Ensure file has restrictive permissions (600)
let metadata = fs::metadata(path)?;
let permissions = metadata.permissions();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if permissions.mode() & 0o777 != 0o600 {
return Err("Config file must have 600 permissions".into());
}
}
let content = fs::read_to_string(path)?;
let config: SecureConfig = toml::from_str(&content)?;
Ok(config)
}
// Load from encrypted file
pub fn load_encrypted(path: &Path, key: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
let encrypted_data = fs::read(path)?;
let decrypted = decrypt_data(&encrypted_data, key)?;
let config: SecureConfig = toml::from_slice(&decrypted)?;
Ok(config)
}
}
// Placeholder for encryption function
fn decrypt_data(data: &[u8], key: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
// Use a proper encryption library like `ring` or `age`
unimplemented!("Implement with proper encryption")
}
Token Rotation
Implement token rotation for enhanced security:
use chrono::{DateTime, Utc, Duration};
pub struct RotatingToken {
current_token: Token,
expires_at: DateTime<Utc>,
refresh_callback: Box<dyn Fn() -> Result<Token, Box<dyn std::error::Error>> + Send + Sync>,
}
impl RotatingToken {
pub fn new<F>(token: Token, expires_at: DateTime<Utc>, refresh_callback: F) -> Self
where
F: Fn() -> Result<Token, Box<dyn std::error::Error>> + Send + Sync + 'static
{
Self {
current_token: token,
expires_at,
refresh_callback: Box::new(refresh_callback),
}
}
pub async fn get_valid_token(&mut self) -> Result<&Token, Box<dyn std::error::Error>> {
if Utc::now() > self.expires_at - Duration::minutes(5) {
// Refresh token before expiration
self.current_token = (self.refresh_callback)()?;
self.expires_at = Utc::now() + Duration::hours(24);
}
Ok(&self.current_token)
}
}
Input Validation
Message Content Validation
Always validate and sanitize user input:
use regex::Regex;
use std::collections::HashSet;
pub struct InputValidator {
url_regex: Regex,
sql_injection_patterns: Vec<Regex>,
max_message_length: usize,
blocked_words: HashSet<String>,
}
impl InputValidator {
pub fn new() -> Self {
let url_regex = Regex::new(r"https?://[^\s]+").unwrap();
let sql_injection_patterns = vec![
Regex::new(r"(?i)(union|select|insert|update|delete|drop|create|alter)\s").unwrap(),
Regex::new(r"(?i)(or|and)\s+\d+\s*=\s*\d+").unwrap(),
Regex::new(r"(?i)'\s*(or|and)\s+'").unwrap(),
];
Self {
url_regex,
sql_injection_patterns,
max_message_length: 2000,
blocked_words: HashSet::new(),
}
}
pub fn validate_message_content(&self, content: &str) -> Result<String, ValidationError> {
// Length check
if content.len() > self.max_message_length {
return Err(ValidationError::TooLong);
}
// Check for SQL injection patterns
for pattern in &self.sql_injection_patterns {
if pattern.is_match(content) {
return Err(ValidationError::SqlInjection);
}
}
// Check for blocked words
let words: Vec<&str> = content.split_whitespace().collect();
for word in words {
if self.blocked_words.contains(&word.to_lowercase()) {
return Err(ValidationError::BlockedContent);
}
}
// Sanitize HTML/markdown
let sanitized = self.sanitize_content(content);
Ok(sanitized)
}
fn sanitize_content(&self, content: &str) -> String {
// Remove or escape potentially dangerous content
content
.replace("<script", "<script")
.replace("javascript:", "")
.replace("data:", "")
}
pub fn extract_safe_urls(&self, content: &str) -> Vec<String> {
let mut safe_urls = Vec::new();
for url_match in self.url_regex.find_iter(content) {
let url = url_match.as_str();
if self.is_safe_url(url) {
safe_urls.push(url.to_string());
}
}
safe_urls
}
fn is_safe_url(&self, url: &str) -> bool {
// Check against whitelist of safe domains
let safe_domains = ["github.com", "docs.rs", "crates.io"];
if let Ok(parsed_url) = url::Url::parse(url) {
if let Some(domain) = parsed_url.domain() {
return safe_domains.iter().any(|&safe| domain.ends_with(safe));
}
}
false
}
}
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
#[error("Message too long")]
TooLong,
#[error("Potential SQL injection detected")]
SqlInjection,
#[error("Blocked content detected")]
BlockedContent,
#[error("Invalid URL")]
InvalidUrl,
}
Command Parameter Validation
Validate command parameters to prevent abuse:
pub struct CommandValidator;
impl CommandValidator {
pub fn validate_user_id(user_id: &str) -> Result<String, ValidationError> {
// QQ user IDs are numeric strings
if user_id.chars().all(|c| c.is_ascii_digit()) && user_id.len() <= 20 {
Ok(user_id.to_string())
} else {
Err(ValidationError::InvalidUserId)
}
}
pub fn validate_channel_id(channel_id: &str) -> Result<String, ValidationError> {
// Similar validation for channel IDs
if channel_id.chars().all(|c| c.is_ascii_digit()) && channel_id.len() <= 20 {
Ok(channel_id.to_string())
} else {
Err(ValidationError::InvalidChannelId)
}
}
pub fn validate_duration(duration_str: &str) -> Result<std::time::Duration, ValidationError> {
use std::time::Duration;
let duration_regex = Regex::new(r"^(\d+)([smhd])$").unwrap();
if let Some(captures) = duration_regex.captures(duration_str) {
let value: u64 = captures[1].parse().map_err(|_| ValidationError::InvalidDuration)?;
let unit = &captures[2];
let duration = match unit {
"s" => Duration::from_secs(value),
"m" => Duration::from_secs(value * 60),
"h" => Duration::from_secs(value * 3600),
"d" => Duration::from_secs(value * 86400),
_ => return Err(ValidationError::InvalidDuration),
};
// Enforce reasonable limits
if duration > Duration::from_secs(86400 * 30) { // 30 days max
return Err(ValidationError::DurationTooLong);
}
Ok(duration)
} else {
Err(ValidationError::InvalidDuration)
}
}
}
Permission Management
Role-Based Access Control
Implement proper permission checking:
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum Permission {
SendMessages,
DeleteMessages,
KickMembers,
BanMembers,
ManageChannels,
ManageGuild,
Administrator,
}
pub struct PermissionManager {
role_permissions: HashMap<String, Vec<Permission>>,
user_roles: HashMap<String, Vec<String>>,
}
impl PermissionManager {
pub fn new() -> Self {
let mut role_permissions = HashMap::new();
// Define default roles and permissions
role_permissions.insert("moderator".to_string(), vec![
Permission::SendMessages,
Permission::DeleteMessages,
Permission::KickMembers,
]);
role_permissions.insert("admin".to_string(), vec![
Permission::SendMessages,
Permission::DeleteMessages,
Permission::KickMembers,
Permission::BanMembers,
Permission::ManageChannels,
Permission::ManageGuild,
]);
role_permissions.insert("owner".to_string(), vec![
Permission::Administrator,
]);
Self {
role_permissions,
user_roles: HashMap::new(),
}
}
pub fn has_permission(&self, user_id: &str, permission: &Permission) -> bool {
if let Some(roles) = self.user_roles.get(user_id) {
for role in roles {
if let Some(perms) = self.role_permissions.get(role) {
if perms.contains(&Permission::Administrator) || perms.contains(permission) {
return true;
}
}
}
}
false
}
pub async fn check_command_permission(
&self,
user_id: &str,
command: &str,
) -> Result<(), PermissionError> {
let required_permission = match command {
"kick" => Permission::KickMembers,
"ban" => Permission::BanMembers,
"delete" => Permission::DeleteMessages,
"mute" => Permission::KickMembers,
_ => Permission::SendMessages,
};
if self.has_permission(user_id, &required_permission) {
Ok(())
} else {
Err(PermissionError::InsufficientPermissions)
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum PermissionError {
#[error("Insufficient permissions")]
InsufficientPermissions,
#[error("User not found")]
UserNotFound,
#[error("Role not found")]
RoleNotFound,
}
Rate Limiting for Security
Implement security-focused rate limiting:
use std::collections::HashMap;
use tokio::time::{Duration, Instant};
pub struct SecurityRateLimiter {
user_attempts: HashMap<String, Vec<Instant>>,
command_cooldowns: HashMap<String, Duration>,
}
impl SecurityRateLimiter {
pub fn new() -> Self {
let mut command_cooldowns = HashMap::new();
command_cooldowns.insert("kick".to_string(), Duration::from_secs(10));
command_cooldowns.insert("ban".to_string(), Duration::from_secs(30));
command_cooldowns.insert("mute".to_string(), Duration::from_secs(5));
Self {
user_attempts: HashMap::new(),
command_cooldowns,
}
}
pub fn check_rate_limit(&mut self, user_id: &str, command: &str) -> Result<(), RateLimitError> {
let now = Instant::now();
let attempts = self.user_attempts.entry(user_id.to_string()).or_insert_with(Vec::new);
// Remove old attempts (older than 1 minute)
attempts.retain(|&attempt| now.duration_since(attempt) < Duration::from_secs(60));
// Check for abuse (more than 10 commands per minute)
if attempts.len() >= 10 {
return Err(RateLimitError::TooManyAttempts);
}
// Check command-specific cooldown
if let Some(cooldown) = self.command_cooldowns.get(command) {
if let Some(&last_attempt) = attempts.last() {
if now.duration_since(last_attempt) < *cooldown {
return Err(RateLimitError::CommandCooldown);
}
}
}
attempts.push(now);
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum RateLimitError {
#[error("Too many attempts")]
TooManyAttempts,
#[error("Command on cooldown")]
CommandCooldown,
}
Data Protection
Sensitive Data Handling
Handle user data securely:
use sha2::{Sha256, Digest};
use std::fmt;
// Never store passwords in plain text
pub struct UserCredentials {
user_id: String,
password_hash: String,
salt: String,
}
impl UserCredentials {
pub fn new(user_id: String, password: &str) -> Self {
let salt = generate_salt();
let password_hash = hash_password(password, &salt);
Self {
user_id,
password_hash,
salt,
}
}
pub fn verify_password(&self, password: &str) -> bool {
let computed_hash = hash_password(password, &self.salt);
computed_hash == self.password_hash
}
}
fn generate_salt() -> String {
use rand::Rng;
let mut rng = rand::thread_rng();
(0..32).map(|_| rng.gen::<u8>()).map(|b| format!("{:02x}", b)).collect()
}
fn hash_password(password: &str, salt: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
hasher.update(salt.as_bytes());
format!("{:x}", hasher.finalize())
}
// Secure logging - avoid logging sensitive data
pub struct SecureLogger;
impl SecureLogger {
pub fn log_user_action(user_id: &str, action: &str, details: Option<&str>) {
// Hash user ID for privacy
let user_hash = Self::hash_user_id(user_id);
match details {
Some(details) => {
tracing::info!("User {} performed action: {} ({})", user_hash, action, details);
}
None => {
tracing::info!("User {} performed action: {}", user_hash, action);
}
}
}
fn hash_user_id(user_id: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(user_id.as_bytes());
format!("{:x}", hasher.finalize())[0..8].to_string()
}
}
Data Encryption
Encrypt sensitive data at rest:
use aes_gcm::{Aes256Gcm, Key, Nonce};
use aes_gcm::aead::{Aead, NewAead};
use rand::RngCore;
pub struct DataEncryption {
cipher: Aes256Gcm,
}
impl DataEncryption {
pub fn new(key: &[u8; 32]) -> Self {
let key = Key::from_slice(key);
let cipher = Aes256Gcm::new(key);
Self { cipher }
}
pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut nonce_bytes = [0u8; 12];
rand::thread_rng().fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = self.cipher.encrypt(nonce, data)?;
// Prepend nonce to ciphertext
let mut result = nonce_bytes.to_vec();
result.extend_from_slice(&ciphertext);
Ok(result)
}
pub fn decrypt(&self, encrypted_data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
if encrypted_data.len() < 12 {
return Err("Invalid encrypted data".into());
}
let (nonce_bytes, ciphertext) = encrypted_data.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
let plaintext = self.cipher.decrypt(nonce, ciphertext)?;
Ok(plaintext)
}
}
Secure Event Handling
Event Validation
Validate all incoming events:
use botrs::{EventHandler, Context, Message, BotError};
pub struct SecureEventHandler {
validator: InputValidator,
permission_manager: PermissionManager,
rate_limiter: SecurityRateLimiter,
}
#[async_trait::async_trait]
impl EventHandler for SecureEventHandler {
async fn message_create(&self, ctx: Context, msg: Message) {
// Validate message source
if let Err(e) = self.validate_message_source(&msg) {
tracing::warn!("Invalid message source: {}", e);
return;
}
// Validate message content
let content = match &msg.content {
Some(content) => content,
None => return,
};
let validated_content = match self.validator.validate_message_content(content) {
Ok(content) => content,
Err(e) => {
tracing::warn!("Message validation failed: {}", e);
self.send_security_warning(&ctx, &msg, "Invalid message content").await;
return;
}
};
// Process validated message
self.process_secure_message(&ctx, &msg, &validated_content).await;
}
async fn error(&self, error: BotError) {
// Log security-relevant errors
match &error {
BotError::AuthenticationFailed(_) => {
tracing::error!("Security alert: Authentication failed - {}", error);
// Alert security team
}
BotError::Forbidden(_) => {
tracing::warn!("Permission denied: {}", error);
}
_ => {
tracing::debug!("Bot error: {}", error);
}
}
}
}
impl SecureEventHandler {
async fn validate_message_source(&self, msg: &Message) -> Result<(), SecurityError> {
// Verify message comes from expected sources
if let Some(guild_id) = &msg.guild_id {
if !self.is_trusted_guild(guild_id) {
return Err(SecurityError::UntrustedSource);
}
}
// Check for bot messages (potential impersonation)
if msg.is_from_bot() {
return Err(SecurityError::BotMessage);
}
Ok(())
}
fn is_trusted_guild(&self, guild_id: &str) -> bool {
// Implement guild whitelist check
true // Placeholder
}
async fn send_security_warning(&self, ctx: &Context, msg: &Message, reason: &str) {
let warning = format!("⚠️ Security warning: {}", reason);
if let Err(e) = msg.reply(&ctx.api, &ctx.token, &warning).await {
tracing::error!("Failed to send security warning: {}", e);
}
}
async fn process_secure_message(&self, ctx: &Context, msg: &Message, content: &str) {
// Implement secure message processing
}
}
#[derive(Debug, thiserror::Error)]
pub enum SecurityError {
#[error("Untrusted message source")]
UntrustedSource,
#[error("Message from bot detected")]
BotMessage,
#[error("Suspicious activity detected")]
SuspiciousActivity,
}
Infrastructure Security
TLS Configuration
Ensure secure connections:
use reqwest::ClientBuilder;
use std::time::Duration;
pub fn create_secure_http_client() -> Result<reqwest::Client, reqwest::Error> {
ClientBuilder::new()
.timeout(Duration::from_secs(30))
.tcp_keepalive(Duration::from_secs(60))
.use_rustls_tls() // Use rustls for better security
.min_tls_version(reqwest::tls::Version::TLS_1_2)
.https_only(true)
.build()
}
Secure Configuration
Implement secure configuration management:
use std::fs;
use std::path::Path;
pub struct SecureConfiguration {
config_path: String,
file_permissions: u32,
}
impl SecureConfiguration {
pub fn new(config_path: &str) -> Self {
Self {
config_path: config_path.to_string(),
file_permissions: 0o600, // Owner read/write only
}
}
pub fn load_config<T>(&self) -> Result<T, Box<dyn std::error::Error>>
where
T: serde::de::DeserializeOwned,
{
let path = Path::new(&self.config_path);
// Verify file permissions
self.verify_file_permissions(path)?;
// Load and parse configuration
let content = fs::read_to_string(path)?;
let config: T = toml::from_str(&content)?;
Ok(config)
}
fn verify_file_permissions(&self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let metadata = fs::metadata(path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = metadata.permissions().mode() & 0o777;
if mode != self.file_permissions {
return Err(format!(
"Insecure file permissions: {:o}, expected: {:o}",
mode, self.file_permissions
).into());
}
}
Ok(())
}
}
Security Monitoring
Audit Logging
Implement comprehensive audit logging:
use chrono::Utc;
use serde::Serialize;
#[derive(Serialize)]
pub struct AuditEvent {
timestamp: String,
event_type: String,
user_id: Option<String>,
action: String,
details: serde_json::Value,
ip_address: Option<String>,
success: bool,
}
pub struct AuditLogger {
log_file: std::fs::File,
}
impl AuditLogger {
pub fn new(log_path: &str) -> Result<Self, std::io::Error> {
let log_file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(log_path)?;
Ok(Self { log_file })
}
pub fn log_security_event(
&mut self,
event_type: &str,
user_id: Option<&str>,
action: &str,
details: serde_json::Value,
success: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let event = AuditEvent {
timestamp: Utc::now().to_rfc3339(),
event_type: event_type.to_string(),
user_id: user_id.map(|s| s.to_string()),
action: action.to_string(),
details,
ip_address: None, // Would be populated from request context
success,
};
writeln!(self.log_file, "{}", serde_json::to_string(&event)?)?;
Ok(())
}
}
Intrusion Detection
Implement basic intrusion detection:
use std::collections::HashMap;
use tokio::time::{Duration, Instant};
pub struct IntrusionDetector {
failed_attempts: HashMap<String, Vec<Instant>>,
blocked_users: HashMap<String, Instant>,
}
impl IntrusionDetector {
pub fn new() -> Self {
Self {
failed_attempts: HashMap::new(),
blocked_users: HashMap::new(),
}
}
pub fn record_failed_attempt(&mut self, user_id: &str) -> bool {
let now = Instant::now();
let attempts = self.failed_attempts.entry(user_id.to_string()).or_insert_with(Vec::new);
// Remove attempts older than 1 hour
attempts.retain(|&attempt| now.duration_since(attempt) < Duration::from_secs(3600));
attempts.push(now);
// Block user if more than 5 failed attempts in 1 hour
if attempts.len() > 5 {
self.blocked_users.insert(user_id.to_string(), now);
tracing::warn!("User {} blocked due to repeated failed attempts", user_id);
true
} else {
false
}
}
pub fn is_blocked(&self, user_id: &str) -> bool {
if let Some(&blocked_at) = self.blocked_users.get(user_id) {
// Block for 24 hours
Instant::now().duration_since(blocked_at) < Duration::from_secs(86400)
} else {
false
}
}
pub fn clear_user_attempts(&mut self, user_id: &str) {
self.failed_attempts.remove(user_id);
self.blocked_users.remove(user_id);
}
}
Security Best Practices
Development Guidelines
- Principle of Least Privilege - Grant minimal necessary permissions
- Defense in Depth - Implement multiple security layers
- Fail Securely - Ensure failures don't expose sensitive information
- Input Validation - Validate all input from untrusted sources
- Output Encoding - Properly encode output to prevent injection
Deployment Security
- Use HTTPS/WSS only - Never transmit credentials over unencrypted connections
- Secure environment variables - Use proper secrets management
- Regular updates - Keep dependencies and runtime updated
- Network isolation - Use firewalls and network segmentation
- Monitoring and alerting - Implement security monitoring
Code Review Checklist
- [ ] No hardcoded credentials or secrets
- [ ] Input validation implemented for all user input
- [ ] Proper error handling without information disclosure
- [ ] Authentication and authorization checks in place
- [ ] Sensitive data properly encrypted/hashed
- [ ] Audit logging for security-relevant events
- [ ] Rate limiting implemented for sensitive operations
- [ ] Dependencies are up to date and from trusted sources
Security Testing
Penetration Testing
Regularly test your bot's security:
// Security test framework
#[cfg(test)]
mod security_tests {
use super::*;
#[tokio::test]
async fn test_sql_injection_protection() {
let validator = InputValidator::new();
let malicious_inputs = vec![
"'; DROP TABLE users; --",
"1' OR '1'='1",
"UNION SELECT password FROM users",
];
for input in malicious_inputs {
assert!(validator.validate_message_content(input).is_err());
}
}
#[tokio::test]
async fn test_rate_limiting() {
let mut rate_limiter = SecurityRateLimiter::new();
// Test rapid-fire attempts
for i in 0..15 {
let result = rate_limiter.check_rate_limit("test_user", "test_command");
if i >= 10 {
assert!(result.is_err()); // Should be rate limited
}
}
}
#[tokio::test]
async fn test_permission_bypass() {
let perm_manager = PermissionManager::new();
// Test that users without permissions can't execute admin commands
assert!(!perm_manager.has_permission("regular_user", &Permission::BanMembers));
assert!(perm_manager.check_command_permission("regular_user", "ban").await.is_err());
}
}
By following these security guidelines and implementing the suggested measures, you can build robust and secure QQ Guild bots that protect both your infrastructure and your users' data.