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.

python
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")
Diagram

Agent Security

📋 Input

🛡️ Injection Defense

🤖 Agent

✅ Audit Log

Security

Supported Versions

Reporting Security Vulnerabilities

If you discover a security vulnerability, please report it responsibly to help protect the community.

  1. Open Draft Advisory

    Navigate to github.com/MervinPraison/PraisonAI/security/advisories/new

  2. Describe the Issue

    Include detailed reproduction steps and impact assessment

  3. 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:

Recent Security Hardening (PR #2062)

python
# 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_command blocks DANGEROUS_COMMANDS by default (allow_dangerous=False)
  • search_replace and append_to_file enforce protected paths
  • Audit log is thread-safe with get_audit_log().close() lifecycle API
  • AgentOps init deduplicated — use is_agentops_available() instead of eager AGENTOPS_AVAILABLE in observability/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, and append_to_file in praisonai.code.tools previously gated their workspace-confinement check behind if workspace: and silently skipped it when no workspace was passed.
  • All three now default the workspace to os.getcwd() when unset, and always enforce is_path_within_directory(...) — matching the behaviour write_file / read_file already 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.

Diagram

👤 raw display name
Hostile newline SYSTEM directive

🧼 neutralize_untrusted_text

✅ safe single line

🧠 sender prefix
into per-turn prompt

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.

python
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 (default 240) — even max_chars <= 3 returns len(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_command shell 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 with PRAISONAI_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 new stop_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) and get_protection_reason(path) in praisonai.security.protected now os.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 as harmless.txt and allowed.
  • Centralised in the guard, so write_file / apply_diff / search_replace all 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-HIGH and 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 only MEDIUM (log + warn, not blocked); they escalate to HIGH only when combined with another signal (for example an authority claim).
  • Two new public helpers ship in praisonai.security.injection: detect_instruction_strict() and detect_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_strings was 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 a WARNING when 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.
Diagram
ToolInjectionDefenseUserToolInjectionDefenseUseralt[truncated (budgetexhausted)][fully scanned, nomatch]tool input (nested / large)walk bounded (bytes + cardinality)block [CRITICAL]forward

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.

Diagram

Yes

No

Hook fires

Raised or
timed out?

🚫 decision=deny
fail closed

✅ hook return value applies

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 share now defaults --redact-level to strict (was standard) because it publishes an HTML transcript for wider distribution. session export still defaults to standard.
  • Broader secret shapes are recognised at both levels: Stripe sk_live_… / sk_test_… keys, the access_key secret word, and key=value pairs whose key contains a secret word (so AWS_SECRET_ACCESS_KEY=… and STRIPE_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_code now applies the full ResourceLimits bundle via resource.setrlimit in a preexec_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, matching achat().
Diagram

Yes

No

Untrusted code

Subprocess

setrlimit caps

Over a cap?

🚫 killed

✅ result

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_blocked now consults both socket.inet_aton and socket.getaddrinfo and blocks if either reads the host as internal.
  • Previously, ipaddress.ip_address refused ambiguous IPv4 forms (0177.0.0.1, 127.1) which then fell through to only the resolver — which reads 0177.0.0.1 as the public 177.0.0.1 and let it through, even though libc-based HTTP clients dial the loopback 127.0.0.1.
Diagram

Yes

No

Host from URL

inet_aton reading

getaddrinfo reading

Either internal?

🚫 refused

✅ fetched

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.

Diagram
ToolApprovalPolicyEngine + guardrailsExecutorToolApprovalPolicyEngine + guardrailsExecutoralt[approval modified args]policy_hook(name, kwargs)allow / rewrite / denyrequest_approval(name, effective_kwargs)approved / modified_argsre-authorise on modified_argsallow / denycall with final kwargsresult

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

Breaking Changes

What changed:

  1. API servers now require authentication by default – anonymous requests return 401
  2. API servers bind to 127.0.0.1 by default instead of 0.0.0.0
  3. MCP rules tools reject unsafe file paths – names like "../../etc/passwd" now fail
  4. 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

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

Breaking / Behavioral Changes

What changed:

  1. Cross-workspace IDs on platform routes now return 404 instead of leaking data – affects project, label, dependency, and issue-activity routes
  2. Generated api.py from praisonai deploy now requires the bleach package at runtime for HTML sanitization
  3. Custom tool eval examples switched from eval to AST allow-list – if you copied the old _safe_eval snippet, adopt the new pattern (see Safe Tool Eval)
  4. WorkspaceService.delete now also removes Member rows 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_files and
  • SubprocessSandbox.write_file / read_file / list_files now 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:

python
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.

Diagram
AuditLogAgentInjectionDefenseUserAuditLogAgentInjectionDefenseUserUser inputCleared input (or blocked)Record tool callResponse

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

python
import os
from praisonaiagents import Agent

agent = Agent(
    api_key=os.environ.get("OPENAI_API_KEY"),
    # ... other config
)

Sandboxed Execution

python
from praisonaiagents import Agent
from praisonaiagents.agent.config import ExecutionConfig

agent = Agent(
    execution=ExecutionConfig(
        code_execution=True,
        code_mode="safe"  # Sandboxed execution
    )
)

Input Validation

python
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

  1. Enable Security Modules

    Always enable security features before creating agents:

    python
    from praisonai.security import enable_security
    enable_security()  # Enables injection defense + audit logging
  2. 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
  3. 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_approval is honoured on every tool call from within the script — a single approval does not silently unlock later calls. The feature is opt-in via code_tools=True and requires an explicit code_tools_allow list:

    python
    execution = ExecutionConfig(
        code_execution=True,
        code_tools=True,
        code_tools_allow=["fetch", "extract_price"],  # Only these tools are callable
    )
  4. 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)
  5. 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

python
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

python
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

python
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

python
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

python
import os
os.environ["LOGLEVEL"] = "debug"
from praisonai.security import enable_security
enable_security()

Staging

python
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

python
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:

Diagram

Report Received

Fix Developed Privately

Publish to PyPI

Publish Advisory

Request CVE

Security Features

Optional Security Modules

python
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:

python
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 0600

The deployment process:

  1. Creates temp YAML via tempfile.mkstemp() with mode 0600 (owner read/write only)
  2. Writes secrets to temp file instead of command line
  3. Passes via --env-vars-file to gcloud command
  4. Removes temp file in finally block, 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 doctor reports each field as available / configured-but-unavailable / configured / missing without revealing the value, so operators can validate wiring before start-up. gateway doctor --fix auto-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.
Secret References

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 code 4029. Loopback is always exempt.
  • UnauthorizedFloodGuard — closes a connection that sends too many unauthorized frames on a single session (max_unauthorized_frames=10), using close code 4028. Distinct from the existing 4008 handshake rate-limit code so clients can handle each case correctly.
  • AuthRateLimiter overflow 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.redis set, max_attempts on 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.
Gateway Edge Protections

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 0o600 is chmod-ed to 0o600 on load. A failing chmod raises OSError instead 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, with icacls resolved from %SystemRoot%\System32 rather than PATH to 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 at INFO, and Windows logs at DEBUG only.

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_TOKEN env 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 finally block 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 0600 instead 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:

python
# Safe concurrent access to chat history
agent.chat_history = new_messages  # Uses locked setter
response = agent.chat("Hello")     # Thread-safe appending

Fail-Closed Approvals

Console-backend approvals now raise PermissionError in async contexts instead of silently bypassing security checks:

python
# 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

Acknowledgments

We thank the security researchers who have responsibly disclosed vulnerabilities. Credits are listed in individual security advisories on GitHub.

Related