Security
Security policies, vulnerability reporting, and best practices for PraisonAI
Harden an Agent by enabling security defaults before it runs — injection defense, audit logging, and sandboxed execution.
from praisonaiagents import Agent
from praisonai.security import enable_security
enable_security() # Injection defense + audit logging
agent = Agent(instructions="Help users safely")
agent.start("Summarise this document")Security
Supported Versions
= 2.0.0)” icon=”check-circle” color=”#22c55e”> Actively maintained with security patches
Reporting Security Vulnerabilities
If you discover a security vulnerability, please report it responsibly to help protect the community.
- Open Draft Advisory
Navigate to github.com/MervinPraison/PraisonAI/security/advisories/new
- Describe the Issue
Include detailed reproduction steps and impact assessment
- Allow Time for Fix
Allow time for maintainers to address the issue before public disclosure
What to Include
Vulnerability Description
Clear explanation of the security issue and affected components
Reproduction Steps
Step-by-step instructions to reproduce the vulnerability
Affected Versions
Which versions of PraisonAI are impacted
Impact Assessment
Severity rating and potential consequences
Suggested Fix
Optional: Your recommendation for fixing the issue
Security Advisories
View all published security advisories:
Browse all published security advisories on GitHub
Report New IssueCreate a new draft security advisory
Recent Security Hardening (PR #2062)
# Before (PR #2062)
hook_id = enable_injection_defense()
remove_hook("before_tool", hook_id)
# After (PR #2062)
hook_ids = enable_injection_defense()
remove_hook("before_tool", hook_ids["before_tool"])
remove_hook("before_agent", hook_ids["before_agent"])Also in PR #2062:
execute_commandblocksDANGEROUS_COMMANDSby default (allow_dangerous=False)search_replaceandappend_to_fileenforce protected paths- Audit log is thread-safe with
get_audit_log().close()lifecycle API - AgentOps init deduplicated — use
is_agentops_available()instead of eagerAGENTOPS_AVAILABLEinobservability/hooks
Recent Security Hardening (PraisonAI #3087)
Commit d1d0272 (fixes #3087) closes a workspace-escape bug in the code-editing tools.
What changed:
apply_diff,search_replace, andappend_to_fileinpraisonai.code.toolspreviously gated their workspace-confinement check behindif workspace:and silently skipped it when no workspace was passed.- All three now default the workspace to
os.getcwd()when unset, and always enforceis_path_within_directory(...)— matching the behaviourwrite_file/read_filealready had.
Who is affected: Anyone using praisonai.code tools (code_apply_diff, code_search_replace, code_write_file, or the low-level append_to_file) without calling set_workspace(...). Absolute paths outside cwd are now rejected with "Path '<path>' is outside the workspace".
Action required: Either call set_workspace("/intended/root") before invoking the tools, or ensure the process cwd is the intended workspace root. No action needed if you were already calling set_workspace(...).
See Code Editing → Workspace Security and Protected Paths.
Recent Security Hardening (PraisonAI #3315)
PR #3315 (fixes #3313) neutralises untrusted platform metadata before it is concatenated into the per-chat attribution prefix.
What changed: In per_chat scope, the bot prepends a [{sender}] prefix to every turn in group / multi-party channels. That prefix now automatically neutralises the raw platform display-name / group-title before formatting: newlines are collapsed to spaces, control characters stripped, whitespace collapsed, and the value length-bounded to 240 characters. Ordinary names ("Bob", "Alice 🙂") render byte-identically — no visible change.
Threat closed: An embedded newline in a hostile group display name previously let the attacker’s name masquerade as a fake markdown heading / system directive that the model re-read every turn — a prompt-injection vector. Group / multi-party channels are the framework’s largest untrusted-input surface; this closes the vector at the one call site that concatenates the name into the prompt.
New public helper: neutralize_untrusted_text(value, *, max_chars=240) from praisonaiagents.session.context. Apply it anywhere you concatenate a user-controlled string into a prompt — a display name, a chat title, a webhook-provided label, an inbound tool argument.
from praisonaiagents.session.context import neutralize_untrusted_text
# Sanitise any untrusted string before interpolating it into a prompt.
safe_label = neutralize_untrusted_text(untrusted_display_name)
prompt = f"[{safe_label}] {user_message}"
# Tighter bound for a value that must stay short (e.g. a tab title):
safe_title = neutralize_untrusted_text(untrusted_title, max_chars=64)Semantics:
- Collapses
\r/\n(and other line separators) to single spaces so a hostile value cannot fake a new line, heading, or system directive. - Strips ASCII / Unicode control characters.
- Collapses runs of whitespace to a single space.
- Hard length bound to
max_chars(default240) — evenmax_chars <= 3returnslen(out) <= max_chars(the"..."marker is dropped in that range). - Non-string inputs are stringified first, so
None, integers, and arbitrary objects are all safe to pass.
Recent Security Hardening (PraisonAI #6157)
PR #6157 (fixes #6156) closes three wrapper-layer gaps:
ImageResult.save()SSRF — URL images are downloaded through a guarded fetch: http(s) only, host screened against loopback/private/metadata addresses, redirects refused, 30 s timeout, 50 MB cap. See Images → Save Image to File.code_execute_commandshell syntax and env scrubbing — unquoted shell operators (>,|,&&, …) now fail with a clear error instead of a false success, and secret-looking environment variables are withheld from the child process (opt specific names back in withPRAISONAI_SHELL_ENV_ALLOWLIST). See Code Editing → code_execute_command.- Schedule runner start-once and shutdown —
ensure_schedule_runner()is lock-protected so concurrent callers start only one poll loop, and the newstop_schedule_runner(timeout=5.0)releases it on shutdown. See Scheduled Run Policy → Start and stop.
Recent Security Hardening (PR #3616)
PR #3616 (fixes #3615) closes a symlink-based bypass in the protected-path guard.
What changed:
is_protected(path)andget_protection_reason(path)inpraisonai.security.protectednowos.path.realpath()the input before matching.- A same-directory symlink to a protected file (for example,
harmless.txt → .env) is now treated as the real target and blocked, instead of being treated asharmless.txtand allowed. - Centralised in the guard, so
write_file/apply_diff/search_replaceall inherit the fix.
Who is affected: Anyone whose agent can write files inside a workspace that also contains protected files (.env, *.pem, id_rsa, etc.) and whose LLM can be talked into creating a symlink first.
Action required: None. On by default. Verify by attempting a write_file("harmless.txt", ...) where harmless.txt is a symlink to .env — the write should now be rejected with the standard protected-path reason (previously it silently overwrote .env).
See Protected Paths.
Recent Security Hardening (PR #3812)
PR #3812 (fixes #3811) splits the injection defense’s Check 1 instruction patterns into strict and soft tiers so benign role-play / coaching prompts stop triggering a block.
What changed:
- Instruction-override patterns now live in two sets. Strict phrases (
ignore all previous instructions,disregard your previous prompt,DAN:,jailbreak,unrestricted mode, …) are unambiguous jailbreaks — a single hit is auto-HIGHand still blocks on one signal. - Soft phrases (
you are now …,pretend to be …,roleplay as …,new instructions:) are common in ordinary prompts. On their own they are onlyMEDIUM(log + warn, not blocked); they escalate toHIGHonly when combined with another signal (for example an authority claim). - Two new public helpers ship in
praisonai.security.injection:detect_instruction_strict()anddetect_instruction_soft().detect_instruction_patterns()stays a back-compat union of both sets.
Who is affected: Anyone running enable_injection_defense() / scan_text(). Benign prompts like "You are now analysing the market data" or "Pretend to be a writing coach" are no longer blocked, while real jailbreaks (ignore all previous instructions, DAN) still block on a single hit.
See Security Best Practices → The 6-Check Injection Pipeline.
Recent Security Hardening (PR #3837)
PR #3837 (fixes #3836) closes a nesting-based bypass in the injection scanner and hardens the before-tool gate to fail closed.
What changed:
InjectionDefense._extract_stringswas recursive and depth-capped at 4 — a payload nested in 5+ dicts was silently unscanned and the tool executed unmediated.- Replaced with an iterative walker (
_extract_strings_bounded) bounded by 1 MiB total scanned bytes (_EXTRACT_MAX_TOTAL_BYTES) and 10 000 strings (_EXTRACT_MAX_STRINGS), not by depth. A legitimately nested tool argument (filter DSL, JSON-schema payload, handoff routing config) is fully scanned; pathological input cannot OOM the process. - Scans dict keys too (attacker-controlled keys reach the prompt in many frameworks), is cycle-safe via a seen-set on container
id(), and logs aWARNINGwhen the budget is exhausted. - The before-tool hook now blocks any call whose input exceeded the scan budget (
HookResult(decision="block", reason="Injection defense: tool input exceeded the scan budget and could not be fully vetted [CRITICAL]")) — a fail-closed gate instead of allow-on-no-match. Allow-on-no-match would let an attacker pad benign strings ahead of an injection payload so the payload is never reached.
Who is affected: Anyone running enable_injection_defense() or scan_text().
Action required: None. On by default. The new fail-closed behaviour may block a tool call that ships an unusually large tool input (>1 MiB of strings, or >10 000 strings). Operators can grep for the log line "[praisonai.security] extraction bounded: ..." to spot it.
The back-compat shim _extract_strings(obj) still returns just the string list (dropping the truncated flag), so external callers of the old private helper keep working; the security hook itself uses the bounded variant so it can observe truncation.
See Security Best Practices → The 6-Check Injection Pipeline.
Recent Security Hardening (PR #3855)
Hook exceptions and timeouts fail closed (PR #3855). Every hook lifecycle path — function hooks, command hooks, and the top-level executor — now returns HookResult(decision="deny", reason=...) on raise or timeout. A crashing BEFORE_TOOL gate no longer allows the tool call through by accident.
This matches the GuardrailChain fail-closed posture and the injection-scanner fail-closed gate from PR #3837 above. See Hooks → Fail-closed behaviour.
Recent Security Hardening (PR #4244)
PR #4244 (fixes #4243) tightens session-share redaction and closes secret-leak gaps in the redactor.
What changed:
praisonai session sharenow defaults--redact-leveltostrict(wasstandard) because it publishes an HTML transcript for wider distribution.session exportstill defaults tostandard.- Broader secret shapes are recognised at both levels: Stripe
sk_live_…/sk_test_…keys, theaccess_keysecret word, andkey=valuepairs whose key contains a secret word (soAWS_SECRET_ACCESS_KEY=…andSTRIPE_SECRET_KEY=…are caught, not only keys that end in one). Secret values are masked as a whole — no half-masking when a value contains/.
Who is affected: Anyone using praisonai session share or session export --sanitise. Shared transcripts are now redacted more aggressively by default.
Action required: None. On by default. Pass praisonai session share <id> --redact-level standard to opt down when the recipient is trusted. See Session → Share a Session.
Recent Security Hardening (PR #4248)
PR #4248 enforces sandbox ResourceLimits on POSIX and mirrors the async throttle/cancel guards onto the sync chat() backend path.
What changed:
- The subprocess sandbox for
execute_codenow applies the fullResourceLimitsbundle viaresource.setrlimitin apreexec_fn. Memory bombs and CPU-bound loops are stopped by the kernel (RLIMIT_AS,RLIMIT_CPU) instead of only by the wall-clock timeout. - Sync
Agent.chat(...)with a managed backend now runs_rate_limiter.acquire()and the cancel-token check before delegating, matchingachat().
Who is affected: Anyone using Agent(execution=ExecutionConfig(code_execution=True, code_mode="safe")) — i.e. any subprocess-sandboxed execute_code call.
Action required: None — on by default on POSIX. On Windows only the timeout applies; a logging.warning is emitted at launch to make the gap visible. See Python Agent → Sandbox Mode.
Since PraisonAI PR #5220, PowerShell cmdlets and cmd built-ins are gated with the same precision as POSIX commands — including -EncodedCommand payloads and & { … } scriptblocks. See Command-Aware Permissions → Windows / PowerShell dialect.
Recent Security Hardening (PR #4966)
PR #4966 (closes GHSA-5c6w-wwfq-7qqm) fixes an octal‑encoded loopback bypass in the spider tools SSRF guard.
What changed:
SpiderTools._host_is_blockednow consults bothsocket.inet_atonandsocket.getaddrinfoand blocks if either reads the host as internal.- Previously,
ipaddress.ip_addressrefused ambiguous IPv4 forms (0177.0.0.1,127.1) which then fell through to only the resolver — which reads0177.0.0.1as the public177.0.0.1and let it through, even though libc-based HTTP clients dial the loopback127.0.0.1.
Who is affected: Anyone using scrape_page, extract_links, crawl, extract_text, or @url: mentions. The bypass allowed an LLM-supplied URL to reach loopback services (e.g. http://0177.0.0.1:<port>/) that the guard was supposed to block.
Action required: None — on by default, no flag, no opt-in. Verify by calling scrape_page("http://0177.0.0.1:8765/") — the tool now returns {'error': 'Invalid or potentially dangerous URL: ...'} instead of a fetch result. See Spider Agent → Built-in URL Safety.
Recent Security Hardening (PR #5238)
PR #5238 (fixes #5064) closes a code-mode gate bypass — model-generated code could reach an allow-listed tool and skip the policy / guardrail checks that block the same call on the direct path.
Who is affected: Anyone using Agent(execution=ExecutionConfig(code_mode="unsafe"|"isolated", code_tools=True)) who relied on PolicyEngine, PermissionManager deny-rules, or per-tool input_guardrails / output_guardrails to block a tool call.
What was wrong: Before PR #5238, only the approval gate ran on the code-mode path (tool_proxy.py::_invoke_with_approval); PolicyEngine.check_tool, agent-wide _tool_call_guardrails, per-tool input_guardrails / output_guardrails, and PermissionManager deny-rules were not consulted. A model could call an allow-listed tool from a code snippet and bypass a guardrail that blocks the same call on the direct path.
What’s fixed: The agent now threads a policy_hook into every code-mode dispatch (in-process and isolated). Gating is identical to the direct tool-call loop — _check_permission_manager_deny → _check_tool_policy_and_guardrails → approval — and an approval that rewrites arguments is re-authorised before dispatch.
What you should do: Upgrade to the release containing PR #5238. Existing code needs no changes. See Code Execution with Tools → Same gates as direct tool calls.
Recent Security Hardening (4.6.34)
PraisonAI 4.6.34 (released May 3, 2026) introduced three critical security fixes that change default behavior:
Security Vulnerabilities Fixed
Critical: Path traversal in MCP rules
GHSA-6rmh-7xcm-cpxjHigh: Unauthenticated API by default
GHSA-3643-7v76-5cj2Medium: SQL identifier injection
Breaking Changes
What changed:
- API servers now require authentication by default – anonymous requests return
401 - API servers bind to
127.0.0.1by default instead of0.0.0.0 - MCP rules tools reject unsafe file paths – names like
"../../etc/passwd"now fail - Knowledge backends validate identifiers – collection names must be
[A-Za-z0-9_]+
Recent Security Hardening (PraisonAI #4088)
Agent.launch() and Agents.launch() now enforce PRAISONAI_LAUNCH_AUTH_TOKEN: every request must send Authorization: Bearer <token> (or X-Auth-Token: <token>) when the token is set, or gets 401. The launched server binds to 127.0.0.1 by default, and if you bind to a non-loopback host without a token the SDK auto-generates a one-time bearer token and prints it to stdout. See Agent API Launch → Authentication.
Recent Security Hardening (next release — Security Batch 10)
PraisonAI’s upcoming release includes 10 security advisories worth of hardening fixes from PR #1684. Several changes are user-facing and breaking.
Security Vulnerabilities Fixed
Critical: execute_code sandbox escapes
GHSA-5c6w-wwfq-7qqmHigh: spider_tools loopback bypass
GHSA-9cr9-25q5-8prjHigh: MCP CLI tools path traversal
GHSA-86qc-r5v2-v6x6High: Call server unauthenticated
GHSA-hvhp-v2gc-268qMedium: write_file workspace escape
GHSA-5cxw-77wg-jrf3Medium: @url: mentions SSRF
GHSA-xwq8-frcg-77q8Low: Cross-workspace IDOR
GHSA-c2m8-4gcg-v22gLow: Platform RBAC bypass
GHSA-xp85-6wwf-r67cLow: CI branch injection
GHSA-3qg8-5g3r-79v5Low: Platform default JWT secret
Breaking Changes
Call server bind hardening (PR #2085, fixes #1602): praisonai call now binds to 127.0.0.1 by default and exposes a --host flag for explicit override. Previously bound to 0.0.0.0 unconditionally while printing a misleading http://localhost:{port} log line. If --host 0.0.0.0 is used without --public, a WARNING is printed at startup. Related advisory: GHSA-86qc-r5v2-v6x6.
Migration for API Server Users
For step-by-step migration instructions, see API Server Authentication.
Recent Security Hardening (Batch 2)
PraisonAI Security Batch 2 (merged May 19, 2026) introduces six additional critical security fixes with behavioral changes:
Security Vulnerabilities Fixed
High: Platform cross-workspace project access
GHSA-5jx9-w35f-vp65High: Platform cross-workspace label access
GHSA-4x6r-9v57-3gqwMedium: Platform cross-workspace dependencies
GHSA-27p4-pjqv-whgjMedium: Platform cross-workspace activity logs
GHSA-6xj3-927j-6pqwMedium: Deploy HTML sanitization missing
GHSA-vg22-4gmj-prxwLow: Tool eval() RCE in examples
Breaking / Behavioral Changes
What changed:
- Cross-workspace IDs on platform routes now return 404 instead of leaking data – affects project, label, dependency, and issue-activity routes
- Generated
api.pyfrompraisonai deploynow requires thebleachpackage at runtime for HTML sanitization - Custom tool eval examples switched from
evalto AST allow-list – if you copied the old_safe_evalsnippet, adopt the new pattern (see Safe Tool Eval) WorkspaceService.deletenow also removesMemberrows in the same transaction – no caller action needed but SDK consumers relying on orphaned memberships should re-check fixtures
Hardened Examples
The unsafe eval(expr, {"__builtins__": {}}) pattern in examples has been replaced with an AST-based allow-list that only permits basic arithmetic operations. See the new Safe Math Eval in Custom Tools guide for the secure implementation pattern.
Recent Security Hardening (PR #1870)
PR #1870 adds filesystem-boundary enforcement to the local sandbox backends.
What changed
DockerSandbox.write_file / read_file / list_filesandSubprocessSandbox.write_file / read_file / list_filesnow reject- any path that resolves outside the sandbox’s
temp_dir. - Rejected operations return
False(write),None(read), or[](list) - and log
Path traversal attempt blocked: <path>. - Helper:
praisonai.sandbox._compat.safe_sandbox_path().
Who is affected
Anyone using sandbox=True, sandbox=SandboxConfig.docker(...), or sandbox=SandboxConfig.subprocess(...) and exposing execute_python_code / execute_shell_command tools to an LLM.
Action required
None — the protection is on by default. If an agent had previously relied on writing to host paths via ../, it will now fail and should be reconfigured to write inside the sandbox root.
Inter-Agent Provenance Envelope (PR #4915)
PR #4915 hardens the agent↔agent boundary so a sub-agent’s output cannot drive the parent as a first-person instruction.
What changed
praisonaiagents.session now exports wrap_inter_agent() and MessageOrigin. create_subagent_tool(...) gained a trusted_sources parameter and now envelopes every sub-agent’s returned text as inter-agent data by default. The envelope is idempotent, bounded (max_chars=8000), and honours a per-source trust override.
Who is affected
Anyone who reads sub-agent output as free-form text. Programs that pattern-match on the raw first line should either strip the envelope, opt the source into trusted_sources, or read the wrapped body.
Migration
None required for the default behaviour. To opt out for a trusted upstream:
from praisonaiagents.tools.subagent_tool import create_subagent_tool
tool = create_subagent_tool(trusted_sources=["worker"])See Inter-Agent Provenance for the full story.
How It Works
Every input passes through injection defense before the Agent acts, and every tool call is recorded in the audit log.
Best Practices
Webhook Verification
Bot adapters that accept inbound webhooks verify HMAC signatures fail-closed by default — missing secrets or invalid signatures return HTTP 401. Set PRAISONAI_INSECURE_WEBHOOKS=true only for local development. See Webhook Verification.
API Key Management
import os
from praisonaiagents import Agent
agent = Agent(
api_key=os.environ.get("OPENAI_API_KEY"),
# ... other config
)Sandboxed Execution
from praisonaiagents import Agent
from praisonaiagents.agent.config import ExecutionConfig
agent = Agent(
execution=ExecutionConfig(
code_execution=True,
code_mode="safe" # Sandboxed execution
)
)Input Validation
from praisonaiagents.security import scan_text
result = scan_text(user_input)
if result.threat_level.value >= 3: # HIGH or CRITICAL
raise ValueError("Potentially malicious input detected")How to Secure Your Agents
Secure Agent Configuration
- Enable Security Modules
Always enable security features before creating agents:
python from praisonai.security import enable_security enable_security() # Enables injection defense + audit logging - Set Workspace Boundaries
Define clear workspace boundaries to prevent path traversal:
python from praisonai.code import set_workspace set_workspace("/safe/project/directory") # Agents cannot escape this - Use ExecutionConfig for Code
When agents need to execute code, always use safe mode:
python from praisonaiagents.agent.config import ExecutionConfig execution = ExecutionConfig( code_execution=True, code_mode="safe", # Sandboxed, not "unsafe" timeout=30 # Limit execution time )When using Code Mode Tool Bridge,
require_approvalis honoured on every tool call from within the script — a single approval does not silently unlock later calls. The feature is opt-in viacode_tools=Trueand requires an explicitcode_tools_allowlist:python execution = ExecutionConfig( code_execution=True, code_tools=True, code_tools_allow=["fetch", "extract_price"], # Only these tools are callable ) - Configure Rate Limiting
Prevent abuse with rate limiting:
python from praisonaiagents.agent.config import ExecutionConfig from praisonaiagents.config import RateLimiter rate_limiter = RateLimiter( max_calls=100, per_seconds=60 ) execution = ExecutionConfig(rate_limiter=rate_limiter) - Add Verification Hooks
For autonomous agents, add verification hooks:
python from praisonaiagents.agent.autonomy import AutonomyConfig autonomy = AutonomyConfig( verification_hooks=[my_custom_validator], max_iterations=10 # Prevent infinite loops )
Security Checklist
Pre-Deployment Checklist
- [ ] Security modules enabled (
enable_security()) - [ ] Workspace boundaries set
- [ ] API keys in environment variables (not hardcoded)
- [ ] Code execution in “safe” mode (if enabled)
- [ ] Rate limiting configured
- [ ] Protected paths reviewed
- [ ] Input validation enabled
Runtime Security Checklist
- [ ] Audit logging active
- [ ] Injection defense scanning all inputs
- [ ] Path traversal protection enabled
- [ ] Tool execution timeout set
- [ ] Memory limits configured
- [ ] Error handling doesn’t leak sensitive info
Production Hardening
- [ ] Network egress restricted
- [ ] Container sandboxing enabled
- [ ] Secrets management (Vault/AWS Secrets/etc.)
- [ ] Log aggregation configured
- [ ] Monitoring and alerting active
- [ ] Regular dependency updates
- [ ] Security scanning in CI/CD
FileMemory user_id sanitisation
FileMemory(user_id=...) strips whitespace, rejects path-traversal characters, and uses the sanitised value for both the in-memory user_id and the on-disk user_path. Empty or whitespace-only inputs fall back to "default". Fix landed in PR #1980 — earlier releases used the raw input for the path, so user_id=" alice " could write to a different directory than mem.user_id reported.
Secure Implementation Guide
Pattern 1: Secure Multi-Agent Workflow
from praisonaiagents import Agent, AgentTeam
from praisonaiagents.agent.config import ExecutionConfig
from praisonaiagents import MemoryConfig
from praisonai.security import enable_security, enable_injection_defense
# Step 1: Enable security globally
enable_security()
# Step 2: Create secure execution config
secure_execution = ExecutionConfig(
code_execution=True,
code_mode="safe",
timeout=30,
rate_limiter=RateLimiter(max_calls=50, per_seconds=60)
)
# Step 3: Configure memory with auto-save
secure_memory = MemoryConfig(
auto_save="session_name", # Persist securely
history=True # Track for audit
)
# Step 4: Create agents with security config
researcher = Agent(
name="researcher",
instructions="Research topics safely",
execution=secure_execution,
memory=secure_memory,
tools=["search_web"] # Whitelist allowed tools
)
writer = Agent(
name="writer",
instructions="Write content based on research",
execution=secure_execution,
memory=secure_memory
)
# Step 5: Create team with handoff security
team = AgentTeam(
agents=[researcher, writer],
handoffs={"researcher": [writer]}, # Explicit handoff allowed
process="sequential" # Controlled flow
)
team.start("Research and write about AI safety")Pattern 2: Secure Bot Deployment
from praisonai import AgentOS
from praisonai.bots import BotConfig
from praisonai.security import enable_security
# Enable security before any bot operations
enable_security()
# Configure bot with security defaults
config = BotConfig(
auto_approve_tools=False, # Require approval for tool calls
allowed_tools=["search_web", "schedule_add"], # Bot-level whitelist (runtime)
max_messages_per_minute=30, # Rate limiting
session_timeout=3600 # 1 hour session limit
)
# Note: BotConfig.allowed_tools provides per-bot runtime filtering
# For global environment-level tool whitelisting, use ALLOWED_TOOLS env var
# See: /features/allowed-tools for comprehensive collision prevention
# Deploy with platform-specific security
botos = AgentOS.from_config("botos.yaml")
botos.run()Pattern 3: Secure Autonomous Agent
from praisonaiagents import Agent
from praisonaiagents.agent.autonomy import AutonomyConfig, AutonomyLevel
from praisonaiagents.agent.config import ExecutionConfig
# Strict execution limits
execution = ExecutionConfig(
code_execution=True,
code_mode="safe",
timeout=10 # Short timeout for autonomous actions
)
# Autonomy with guardrails
autonomy = AutonomyConfig(
level=AutonomyLevel.SUGGEST, # Start conservative
auto_escalate=False, # Manual approval for escalation
max_iterations=5, # Prevent runaway loops
verification_hooks=[
lambda action: validate_action_safe(action),
lambda action: check_resource_limits(action)
]
)
agent = Agent(
name="autonomous_assistant",
instructions="Help users autonomously within strict bounds",
execution=execution,
autonomy=autonomy,
tools=["read_file", "write_file"] # Limited toolset
)
# Run with explicit user approval loop
agent.run_autonomous(
prompt="Organize my project files",
require_approval=True # Each action needs approval
)Pattern 4: Secure MCP Server Integration
from praisonaiagents import Agent
from praisonaiagents.mcp import MCP
# Whitelist only specific MCP servers
allowed_mcp_servers = [
MCP(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem"],
env={"ALLOWED_PATHS": "/safe/data"} # Restrict server access
)
]
agent = Agent(
name="mcp_agent",
instructions="Use filesystem tools safely",
tools=allowed_mcp_servers,
execution=ExecutionConfig(
code_execution=False # Disable code exec when using MCP
)
)Security Hardening by Environment
Development
Set LOGLEVEL=debug for security event visibility
Use Test API KeysSeparate keys with limited quotas for dev
import os
os.environ["LOGLEVEL"] = "debug"
from praisonai.security import enable_security
enable_security()Staging
Enable full audit logging without blocking
Test ProtectionVerify all security controls work
from praisonai.security import enable_audit_log
enable_audit_log()Sensitive keys (api keys, tokens, passwords) in tool inputs are redacted by default; see Audit Logging to customise.
Production
Enable blocking for all threat levels
Minimal LogsReduce verbose logging, keep audit trail
from praisonai.security import enable_security
from praisonaiagents.security import InjectionDefense
# Production: block on medium+ threats
defense = InjectionDefense(blocking_threshold="MEDIUM")
enable_security(defense)CVE Policy
PraisonAI follows responsible disclosure practices with a structured workflow:
Security Features
Prevents agents from accessing files outside the designated workspace
Injection DefenseScans prompts and tool inputs (including dict keys) for prompt injection before processing — cycle-safe and fail-closed on oversized inputs
Protected PathsBlocks modification of sensitive system files and directories
Audit LoggingThread-safe JSONL audit log for tool calls
Untrusted Text Neutralisationneutralize_untrusted_text sanitises platform metadata before it reaches a prompt
Inbound webhook / hook payloads wrapped in <external_request_payload> — the ingress counterpart to <external_tool_result>
Optional Security Modules
from praisonai.security import enable_security, enable_injection_defense
# Enable all security features
enable_security()
# Or enable specific features
enable_injection_defense()Secret-file read gate
The core PermissionManager gates reads of common credential-file globs (.env, .env, .env., .pem, .key, id_rsa, id_dsa, id_ecdsa, id_ed25519, .pfx, .p12) to ask by default. Safe example/template siblings (.env.example, .env.sample, .env.template, .example, .sample, .template) are not gated. A broad "read:": "allow" rule does not authorise these — you must opt in with a secret-specific rule ("read:.env": "allow"), a persistent approval, or an explicit deny to harden. See Permissions → Built-in Secret-File Read Gate.
Secrets in Deployment
PraisonAI never passes secrets via command line arguments, which prevents exposure in process lists, shell history, and CI logs.
Secure Cloud Deployment
When deploying to cloud platforms, PraisonAI automatically handles secrets securely:
from praisonai.deploy import Deploy, DeployConfig, DeployType, CloudProvider
config = DeployConfig(
type=DeployType.CLOUD,
cloud=CloudConfig(
provider=CloudProvider.GCP,
# Secrets are handled via secure temp files, not argv
env_vars={
"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY"),
"DATABASE_URL": os.getenv("DATABASE_URL")
}
)
)
deploy = Deploy(config, "agents.yaml")
result = deploy.deploy() # Uses --env-vars-file with chmod 0600The deployment process:
- Creates temp YAML via
tempfile.mkstemp()with mode0600(owner read/write only) - Writes secrets to temp file instead of command line
- Passes via
--env-vars-filetogcloudcommand - Removes temp file in
finallyblock, even if deployment fails
Reference: See GCP Deploy Documentation for implementation details.
Secret handling for gateway credentials
Gateway channel credentials — token, app_token, and verify_token — support a first-class { source, id } reference so tokens never live in YAML or a process-wide env var.
- Zero secrets in YAML, zero in the process env.
{ source: file, id: /run/secrets/token }reads a mounted Docker / Kubernetes secret;{ source: exec, id: "vault read ..." }runs a secret-manager CLI. Neither is inherited by child processes. - Automatic log redaction. Every resolved secret is registered so it is scrubbed from logs and tracebacks — tokens never leak into errors.
- Availability probe never prints values.
praisonai gateway doctorreports each field asavailable/configured-but-unavailable/configured/missingwithout revealing the value, so operators can validate wiring before start-up.gateway doctor --fixauto-mints a strong token when the current one is weak/missing and re-validates, without printing the value — see Gateway CLI › Auto-repair. - Weak / placeholder secrets are rejected. Publicly-known placeholder secrets (
change-me,your-token-here, the literal$(openssl rand -hex 16)) are rejected on external binds — see Weak Secret Guard.
Load gateway credentials from files, env vars, or secret managers — never plaintext
Gateway Edge Protections (PR #2623)
Internet-exposed praisonai serve gateway deployments now have two default-on guards that require no configuration:
PreauthConnectionBudget— caps concurrent unauthenticated WebSocket connections per source IP (preauth_max_connections_per_ip=32). Connections that exceed the cap are closed before auth runs with WebSocket close code4029. Loopback is always exempt.UnauthorizedFloodGuard— closes a connection that sends too many unauthorized frames on a single session (max_unauthorized_frames=10), using close code4028. Distinct from the existing4008handshake rate-limit code so clients can handle each case correctly.AuthRateLimiteroverflow fail-closed — when the per-IP key map is saturated, new IPs are rejected rather than admitted (which would evict existing lockouts). Existing lockouts are preserved even under a fresh-IP flood.- Cluster-wide auth budgets — with
gateway.push.redisset,max_attemptson approval, WebSocket-upgrade and magic-link auth is a total across every replica; without it, each replica keeps its own budget. See Cluster-Wide Auth Rate Limiting.
Full operator guide: defaults, YAML/Python tuning, close code reference, and when to change limits
Recent Security Hardening (PR #2850)
PR #2850 (fixes #2843) upgrades .gateway_secret handling in praisonai_bot.gateway.pairing from warn-only to remediate-or-fail-closed. The file signs pairing codes, so world-readable permissions previously allowed forged codes; the old behaviour surfaced the risk but loaded the file anyway.
Permissions are now re-checked and remediated on every PairingStore(...) construction, not only at creation:
- POSIX: an existing file with any mode other than
0o600ischmod-ed to0o600on load. A failingchmodraisesOSErrorinstead of falling back to secret regeneration — preserving the HMAC signing key so outstanding pairing codes stay valid. - Windows: best-effort ACL lockdown via
icacls /inheritance:r /grant:r "<user>":F, withicaclsresolved from%SystemRoot%\System32rather thanPATHto avoid a hijacked binary. This path is best-effort and never raises. - The prior per-init
WARNING("Gateway secret file … has insecure permissions …") is removed; POSIX success logs atINFO, and Windows logs atDEBUGonly.
Pairing codes and the inline Approve/Deny callback buttons sign with this same persisted secret at ~/.praisonai/gateway/.gateway_secret (mode 0600), so both survive gateway restarts by default — no operator action required. PRAISONAI_CALLBACK_SECRET is only needed to share one secret across multiple hosts/replicas (or to override for rotation). See Bot Pairing → Secret Management for pairing codes and Interactive Bot Actions for the callback-button flow.
PR #3974 further hardens the pairing store by hashing codes at rest and adding a store-level brute-force lockout. Legacy plaintext stores still load, so there is nothing to run — see Bot Security → Gateway Pairing.
Recent Security Hardening
Recent security improvements include:
Gateway Auth Token Unification (PR #1744):
- Config token now exports to
GATEWAY_AUTH_TOKENenv var at startup - Ensures all auth paths (HTTP, magic-link, WebSocket) use the same secret
- Config wins over environment variable to prevent stale token issues
Sandbox Resource Cleanup (PR #1744):
- SSH: Fixed temp file cleanup in
finallyblock and remote process timeout handling - Docker: Container naming and proper kill on timeout to enforce resource limits
- Prevents resource leaks that could lead to DoS conditions
Cloud Deploy Secret Handling (PR #1744):
- Uses secure temp YAML file with
chmod 0600instead of argv secrets - Prevents secret exposure in
ps, shell history, and CI logs - Automatic cleanup even on deployment failures
Concurrency & Async Safety
PraisonAI includes several security hardening measures for concurrent and asynchronous execution:
Chat-History Integrity
Chat history mutations are now lock-protected to prevent lost updates and corrupt rollback in multi-agent and async scenarios:
# Safe concurrent access to chat history
agent.chat_history = new_messages # Uses locked setter
response = agent.chat("Hello") # Thread-safe appendingFail-Closed Approvals
Console-backend approvals now raise PermissionError in async contexts instead of silently bypassing security checks:
# Configure non-console backend for async compatibility
from praisonaiagents.approval import get_approval_registry, WebhookBackend
get_approval_registry().set_backend(
WebhookBackend(url="https://your-approval-service.com/approve")
)See the Approval guide for more async-compatible backends.
Visible Failures
Previously silent failures now log at WARN level with stack traces for improved security visibility:
- Streaming callback errors (
praisonaiagents.streaming.events) - Session-gateway linkage failures (
praisonaiagents.agent.execution_mixin) - Learning-nudge generation failures (
praisonaiagents.agent.tool_execution)
Monitor these log channels for potential security incidents or system anomalies.
Resources
Source code, issue tracking, and contribution guidelines
DocumentationComprehensive guides, API reference, and tutorials
WebsiteProduct overview, features, and enterprise solutions
Security AdvisoriesView and report security issues
Acknowledgments
We thank the security researchers who have responsibly disclosed vulnerabilities. Credits are listed in individual security advisories on GitHub.