Merge PR #77: add security hardening (SECURITY.md, workspace restriction for file tools)
This commit is contained in:
commit
5f5536c0d1
2
.gitignore
vendored
2
.gitignore
vendored
@ -15,3 +15,5 @@ docs/
|
|||||||
*.pyzz
|
*.pyzz
|
||||||
.venv/
|
.venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
poetry.lock
|
||||||
|
.pytest_cache/
|
||||||
|
|||||||
264
SECURITY.md
Normal file
264
SECURITY.md
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover a security vulnerability in nanobot, please report it by:
|
||||||
|
|
||||||
|
1. **DO NOT** open a public GitHub issue
|
||||||
|
2. Create a private security advisory on GitHub or contact the repository maintainers
|
||||||
|
3. Include:
|
||||||
|
- Description of the vulnerability
|
||||||
|
- Steps to reproduce
|
||||||
|
- Potential impact
|
||||||
|
- Suggested fix (if any)
|
||||||
|
|
||||||
|
We aim to respond to security reports within 48 hours.
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
### 1. API Key Management
|
||||||
|
|
||||||
|
**CRITICAL**: Never commit API keys to version control.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ✅ Good: Store in config file with restricted permissions
|
||||||
|
chmod 600 ~/.nanobot/config.json
|
||||||
|
|
||||||
|
# ❌ Bad: Hardcoding keys in code or committing them
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommendations:**
|
||||||
|
- Store API keys in `~/.nanobot/config.json` with file permissions set to `0600`
|
||||||
|
- Consider using environment variables for sensitive keys
|
||||||
|
- Use OS keyring/credential manager for production deployments
|
||||||
|
- Rotate API keys regularly
|
||||||
|
- Use separate API keys for development and production
|
||||||
|
|
||||||
|
### 2. Channel Access Control
|
||||||
|
|
||||||
|
**IMPORTANT**: Always configure `allowFrom` lists for production use.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"telegram": {
|
||||||
|
"enabled": true,
|
||||||
|
"token": "YOUR_BOT_TOKEN",
|
||||||
|
"allowFrom": ["123456789", "987654321"]
|
||||||
|
},
|
||||||
|
"whatsapp": {
|
||||||
|
"enabled": true,
|
||||||
|
"allowFrom": ["+1234567890"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Security Notes:**
|
||||||
|
- Empty `allowFrom` list will **ALLOW ALL** users (open by default for personal use)
|
||||||
|
- Get your Telegram user ID from `@userinfobot`
|
||||||
|
- Use full phone numbers with country code for WhatsApp
|
||||||
|
- Review access logs regularly for unauthorized access attempts
|
||||||
|
|
||||||
|
### 3. Shell Command Execution
|
||||||
|
|
||||||
|
The `exec` tool can execute shell commands. While dangerous command patterns are blocked, you should:
|
||||||
|
|
||||||
|
- ✅ Review all tool usage in agent logs
|
||||||
|
- ✅ Understand what commands the agent is running
|
||||||
|
- ✅ Use a dedicated user account with limited privileges
|
||||||
|
- ✅ Never run nanobot as root
|
||||||
|
- ❌ Don't disable security checks
|
||||||
|
- ❌ Don't run on systems with sensitive data without careful review
|
||||||
|
|
||||||
|
**Blocked patterns:**
|
||||||
|
- `rm -rf /` - Root filesystem deletion
|
||||||
|
- Fork bombs
|
||||||
|
- Filesystem formatting (`mkfs.*`)
|
||||||
|
- Raw disk writes
|
||||||
|
- Other destructive operations
|
||||||
|
|
||||||
|
### 4. File System Access
|
||||||
|
|
||||||
|
File operations have path traversal protection, but:
|
||||||
|
|
||||||
|
- ✅ Run nanobot with a dedicated user account
|
||||||
|
- ✅ Use filesystem permissions to protect sensitive directories
|
||||||
|
- ✅ Regularly audit file operations in logs
|
||||||
|
- ❌ Don't give unrestricted access to sensitive files
|
||||||
|
|
||||||
|
### 5. Network Security
|
||||||
|
|
||||||
|
**API Calls:**
|
||||||
|
- All external API calls use HTTPS by default
|
||||||
|
- Timeouts are configured to prevent hanging requests
|
||||||
|
- Consider using a firewall to restrict outbound connections if needed
|
||||||
|
|
||||||
|
**WhatsApp Bridge:**
|
||||||
|
- The bridge runs on `localhost:3001` by default
|
||||||
|
- If exposing to network, use proper authentication and TLS
|
||||||
|
- Keep authentication data in `~/.nanobot/whatsapp-auth` secure (mode 0700)
|
||||||
|
|
||||||
|
### 6. Dependency Security
|
||||||
|
|
||||||
|
**Critical**: Keep dependencies updated!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check for vulnerable dependencies
|
||||||
|
pip install pip-audit
|
||||||
|
pip-audit
|
||||||
|
|
||||||
|
# Update to latest secure versions
|
||||||
|
pip install --upgrade nanobot-ai
|
||||||
|
```
|
||||||
|
|
||||||
|
For Node.js dependencies (WhatsApp bridge):
|
||||||
|
```bash
|
||||||
|
cd bridge
|
||||||
|
npm audit
|
||||||
|
npm audit fix
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Notes:**
|
||||||
|
- Keep `litellm` updated to the latest version for security fixes
|
||||||
|
- We've updated `ws` to `>=8.17.1` to fix DoS vulnerability
|
||||||
|
- Run `pip-audit` or `npm audit` regularly
|
||||||
|
- Subscribe to security advisories for nanobot and its dependencies
|
||||||
|
|
||||||
|
### 7. Production Deployment
|
||||||
|
|
||||||
|
For production use:
|
||||||
|
|
||||||
|
1. **Isolate the Environment**
|
||||||
|
```bash
|
||||||
|
# Run in a container or VM
|
||||||
|
docker run --rm -it python:3.11
|
||||||
|
pip install nanobot-ai
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Use a Dedicated User**
|
||||||
|
```bash
|
||||||
|
sudo useradd -m -s /bin/bash nanobot
|
||||||
|
sudo -u nanobot nanobot gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Set Proper Permissions**
|
||||||
|
```bash
|
||||||
|
chmod 700 ~/.nanobot
|
||||||
|
chmod 600 ~/.nanobot/config.json
|
||||||
|
chmod 700 ~/.nanobot/whatsapp-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Enable Logging**
|
||||||
|
```bash
|
||||||
|
# Configure log monitoring
|
||||||
|
tail -f ~/.nanobot/logs/nanobot.log
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Use Rate Limiting**
|
||||||
|
- Configure rate limits on your API providers
|
||||||
|
- Monitor usage for anomalies
|
||||||
|
- Set spending limits on LLM APIs
|
||||||
|
|
||||||
|
6. **Regular Updates**
|
||||||
|
```bash
|
||||||
|
# Check for updates weekly
|
||||||
|
pip install --upgrade nanobot-ai
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Development vs Production
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
- Use separate API keys
|
||||||
|
- Test with non-sensitive data
|
||||||
|
- Enable verbose logging
|
||||||
|
- Use a test Telegram bot
|
||||||
|
|
||||||
|
**Production:**
|
||||||
|
- Use dedicated API keys with spending limits
|
||||||
|
- Restrict file system access
|
||||||
|
- Enable audit logging
|
||||||
|
- Regular security reviews
|
||||||
|
- Monitor for unusual activity
|
||||||
|
|
||||||
|
### 9. Data Privacy
|
||||||
|
|
||||||
|
- **Logs may contain sensitive information** - secure log files appropriately
|
||||||
|
- **LLM providers see your prompts** - review their privacy policies
|
||||||
|
- **Chat history is stored locally** - protect the `~/.nanobot` directory
|
||||||
|
- **API keys are in plain text** - use OS keyring for production
|
||||||
|
|
||||||
|
### 10. Incident Response
|
||||||
|
|
||||||
|
If you suspect a security breach:
|
||||||
|
|
||||||
|
1. **Immediately revoke compromised API keys**
|
||||||
|
2. **Review logs for unauthorized access**
|
||||||
|
```bash
|
||||||
|
grep "Access denied" ~/.nanobot/logs/nanobot.log
|
||||||
|
```
|
||||||
|
3. **Check for unexpected file modifications**
|
||||||
|
4. **Rotate all credentials**
|
||||||
|
5. **Update to latest version**
|
||||||
|
6. **Report the incident** to maintainers
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
### Built-in Security Controls
|
||||||
|
|
||||||
|
✅ **Input Validation**
|
||||||
|
- Path traversal protection on file operations
|
||||||
|
- Dangerous command pattern detection
|
||||||
|
- Input length limits on HTTP requests
|
||||||
|
|
||||||
|
✅ **Authentication**
|
||||||
|
- Allow-list based access control
|
||||||
|
- Failed authentication attempt logging
|
||||||
|
- Open by default (configure allowFrom for production use)
|
||||||
|
|
||||||
|
✅ **Resource Protection**
|
||||||
|
- Command execution timeouts (60s default)
|
||||||
|
- Output truncation (10KB limit)
|
||||||
|
- HTTP request timeouts (10-30s)
|
||||||
|
|
||||||
|
✅ **Secure Communication**
|
||||||
|
- HTTPS for all external API calls
|
||||||
|
- TLS for Telegram API
|
||||||
|
- WebSocket security for WhatsApp bridge
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
⚠️ **Current Security Limitations:**
|
||||||
|
|
||||||
|
1. **No Rate Limiting** - Users can send unlimited messages (add your own if needed)
|
||||||
|
2. **Plain Text Config** - API keys stored in plain text (use keyring for production)
|
||||||
|
3. **No Session Management** - No automatic session expiry
|
||||||
|
4. **Limited Command Filtering** - Only blocks obvious dangerous patterns
|
||||||
|
5. **No Audit Trail** - Limited security event logging (enhance as needed)
|
||||||
|
|
||||||
|
## Security Checklist
|
||||||
|
|
||||||
|
Before deploying nanobot:
|
||||||
|
|
||||||
|
- [ ] API keys stored securely (not in code)
|
||||||
|
- [ ] Config file permissions set to 0600
|
||||||
|
- [ ] `allowFrom` lists configured for all channels
|
||||||
|
- [ ] Running as non-root user
|
||||||
|
- [ ] File system permissions properly restricted
|
||||||
|
- [ ] Dependencies updated to latest secure versions
|
||||||
|
- [ ] Logs monitored for security events
|
||||||
|
- [ ] Rate limits configured on API providers
|
||||||
|
- [ ] Backup and disaster recovery plan in place
|
||||||
|
- [ ] Security review of custom skills/tools
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
**Last Updated**: 2026-02-03
|
||||||
|
|
||||||
|
For the latest security updates and announcements, check:
|
||||||
|
- GitHub Security Advisories: https://github.com/HKUDS/nanobot/security/advisories
|
||||||
|
- Release Notes: https://github.com/HKUDS/nanobot/releases
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See LICENSE file for details.
|
||||||
@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
||||||
"ws": "^8.17.0",
|
"ws": "^8.17.1",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"qrcode-terminal": "^0.12.0",
|
||||||
"pino": "^9.0.0"
|
"pino": "^9.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -73,11 +73,12 @@ class AgentLoop:
|
|||||||
|
|
||||||
def _register_default_tools(self) -> None:
|
def _register_default_tools(self) -> None:
|
||||||
"""Register the default set of tools."""
|
"""Register the default set of tools."""
|
||||||
# File tools
|
# File tools (restrict to workspace if configured)
|
||||||
self.tools.register(ReadFileTool())
|
allowed_dir = self.workspace if self.exec_config.restrict_to_workspace else None
|
||||||
self.tools.register(WriteFileTool())
|
self.tools.register(ReadFileTool(allowed_dir=allowed_dir))
|
||||||
self.tools.register(EditFileTool())
|
self.tools.register(WriteFileTool(allowed_dir=allowed_dir))
|
||||||
self.tools.register(ListDirTool())
|
self.tools.register(EditFileTool(allowed_dir=allowed_dir))
|
||||||
|
self.tools.register(ListDirTool(allowed_dir=allowed_dir))
|
||||||
|
|
||||||
# Shell tool
|
# Shell tool
|
||||||
self.tools.register(ExecTool(
|
self.tools.register(ExecTool(
|
||||||
|
|||||||
@ -96,9 +96,10 @@ class SubagentManager:
|
|||||||
try:
|
try:
|
||||||
# Build subagent tools (no message tool, no spawn tool)
|
# Build subagent tools (no message tool, no spawn tool)
|
||||||
tools = ToolRegistry()
|
tools = ToolRegistry()
|
||||||
tools.register(ReadFileTool())
|
allowed_dir = self.workspace if self.exec_config.restrict_to_workspace else None
|
||||||
tools.register(WriteFileTool())
|
tools.register(ReadFileTool(allowed_dir=allowed_dir))
|
||||||
tools.register(ListDirTool())
|
tools.register(WriteFileTool(allowed_dir=allowed_dir))
|
||||||
|
tools.register(ListDirTool(allowed_dir=allowed_dir))
|
||||||
tools.register(ExecTool(
|
tools.register(ExecTool(
|
||||||
working_dir=str(self.workspace),
|
working_dir=str(self.workspace),
|
||||||
timeout=self.exec_config.timeout,
|
timeout=self.exec_config.timeout,
|
||||||
|
|||||||
@ -6,9 +6,20 @@ from typing import Any
|
|||||||
from nanobot.agent.tools.base import Tool
|
from nanobot.agent.tools.base import Tool
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_path(path: str, allowed_dir: Path | None = None) -> Path:
|
||||||
|
"""Resolve path and optionally enforce directory restriction."""
|
||||||
|
resolved = Path(path).expanduser().resolve()
|
||||||
|
if allowed_dir and not str(resolved).startswith(str(allowed_dir.resolve())):
|
||||||
|
raise PermissionError(f"Path {path} is outside allowed directory {allowed_dir}")
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
class ReadFileTool(Tool):
|
class ReadFileTool(Tool):
|
||||||
"""Tool to read file contents."""
|
"""Tool to read file contents."""
|
||||||
|
|
||||||
|
def __init__(self, allowed_dir: Path | None = None):
|
||||||
|
self._allowed_dir = allowed_dir
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return "read_file"
|
return "read_file"
|
||||||
@ -32,7 +43,7 @@ class ReadFileTool(Tool):
|
|||||||
|
|
||||||
async def execute(self, path: str, **kwargs: Any) -> str:
|
async def execute(self, path: str, **kwargs: Any) -> str:
|
||||||
try:
|
try:
|
||||||
file_path = Path(path).expanduser()
|
file_path = _resolve_path(path, self._allowed_dir)
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
return f"Error: File not found: {path}"
|
return f"Error: File not found: {path}"
|
||||||
if not file_path.is_file():
|
if not file_path.is_file():
|
||||||
@ -40,8 +51,8 @@ class ReadFileTool(Tool):
|
|||||||
|
|
||||||
content = file_path.read_text(encoding="utf-8")
|
content = file_path.read_text(encoding="utf-8")
|
||||||
return content
|
return content
|
||||||
except PermissionError:
|
except PermissionError as e:
|
||||||
return f"Error: Permission denied: {path}"
|
return f"Error: {e}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error reading file: {str(e)}"
|
return f"Error reading file: {str(e)}"
|
||||||
|
|
||||||
@ -49,6 +60,9 @@ class ReadFileTool(Tool):
|
|||||||
class WriteFileTool(Tool):
|
class WriteFileTool(Tool):
|
||||||
"""Tool to write content to a file."""
|
"""Tool to write content to a file."""
|
||||||
|
|
||||||
|
def __init__(self, allowed_dir: Path | None = None):
|
||||||
|
self._allowed_dir = allowed_dir
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return "write_file"
|
return "write_file"
|
||||||
@ -76,12 +90,12 @@ class WriteFileTool(Tool):
|
|||||||
|
|
||||||
async def execute(self, path: str, content: str, **kwargs: Any) -> str:
|
async def execute(self, path: str, content: str, **kwargs: Any) -> str:
|
||||||
try:
|
try:
|
||||||
file_path = Path(path).expanduser()
|
file_path = _resolve_path(path, self._allowed_dir)
|
||||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
file_path.write_text(content, encoding="utf-8")
|
file_path.write_text(content, encoding="utf-8")
|
||||||
return f"Successfully wrote {len(content)} bytes to {path}"
|
return f"Successfully wrote {len(content)} bytes to {path}"
|
||||||
except PermissionError:
|
except PermissionError as e:
|
||||||
return f"Error: Permission denied: {path}"
|
return f"Error: {e}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error writing file: {str(e)}"
|
return f"Error writing file: {str(e)}"
|
||||||
|
|
||||||
@ -89,6 +103,9 @@ class WriteFileTool(Tool):
|
|||||||
class EditFileTool(Tool):
|
class EditFileTool(Tool):
|
||||||
"""Tool to edit a file by replacing text."""
|
"""Tool to edit a file by replacing text."""
|
||||||
|
|
||||||
|
def __init__(self, allowed_dir: Path | None = None):
|
||||||
|
self._allowed_dir = allowed_dir
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return "edit_file"
|
return "edit_file"
|
||||||
@ -120,7 +137,7 @@ class EditFileTool(Tool):
|
|||||||
|
|
||||||
async def execute(self, path: str, old_text: str, new_text: str, **kwargs: Any) -> str:
|
async def execute(self, path: str, old_text: str, new_text: str, **kwargs: Any) -> str:
|
||||||
try:
|
try:
|
||||||
file_path = Path(path).expanduser()
|
file_path = _resolve_path(path, self._allowed_dir)
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
return f"Error: File not found: {path}"
|
return f"Error: File not found: {path}"
|
||||||
|
|
||||||
@ -138,8 +155,8 @@ class EditFileTool(Tool):
|
|||||||
file_path.write_text(new_content, encoding="utf-8")
|
file_path.write_text(new_content, encoding="utf-8")
|
||||||
|
|
||||||
return f"Successfully edited {path}"
|
return f"Successfully edited {path}"
|
||||||
except PermissionError:
|
except PermissionError as e:
|
||||||
return f"Error: Permission denied: {path}"
|
return f"Error: {e}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error editing file: {str(e)}"
|
return f"Error editing file: {str(e)}"
|
||||||
|
|
||||||
@ -147,6 +164,9 @@ class EditFileTool(Tool):
|
|||||||
class ListDirTool(Tool):
|
class ListDirTool(Tool):
|
||||||
"""Tool to list directory contents."""
|
"""Tool to list directory contents."""
|
||||||
|
|
||||||
|
def __init__(self, allowed_dir: Path | None = None):
|
||||||
|
self._allowed_dir = allowed_dir
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return "list_dir"
|
return "list_dir"
|
||||||
@ -170,7 +190,7 @@ class ListDirTool(Tool):
|
|||||||
|
|
||||||
async def execute(self, path: str, **kwargs: Any) -> str:
|
async def execute(self, path: str, **kwargs: Any) -> str:
|
||||||
try:
|
try:
|
||||||
dir_path = Path(path).expanduser()
|
dir_path = _resolve_path(path, self._allowed_dir)
|
||||||
if not dir_path.exists():
|
if not dir_path.exists():
|
||||||
return f"Error: Directory not found: {path}"
|
return f"Error: Directory not found: {path}"
|
||||||
if not dir_path.is_dir():
|
if not dir_path.is_dir():
|
||||||
@ -185,7 +205,7 @@ class ListDirTool(Tool):
|
|||||||
return f"Directory {path} is empty"
|
return f"Directory {path} is empty"
|
||||||
|
|
||||||
return "\n".join(items)
|
return "\n".join(items)
|
||||||
except PermissionError:
|
except PermissionError as e:
|
||||||
return f"Error: Permission denied: {path}"
|
return f"Error: {e}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error listing directory: {str(e)}"
|
return f"Error listing directory: {str(e)}"
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from nanobot.bus.events import InboundMessage, OutboundMessage
|
from nanobot.bus.events import InboundMessage, OutboundMessage
|
||||||
from nanobot.bus.queue import MessageBus
|
from nanobot.bus.queue import MessageBus
|
||||||
|
|
||||||
@ -102,6 +104,10 @@ class BaseChannel(ABC):
|
|||||||
metadata: Optional channel-specific metadata.
|
metadata: Optional channel-specific metadata.
|
||||||
"""
|
"""
|
||||||
if not self.is_allowed(sender_id):
|
if not self.is_allowed(sender_id):
|
||||||
|
logger.warning(
|
||||||
|
f"Access denied for sender {sender_id} on channel {self.name}. "
|
||||||
|
f"Add them to allowFrom list in config to grant access."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
msg = InboundMessage(
|
msg = InboundMessage(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user