Portal Community

Threat Model

ThreatRiskControl
SSRF via browser navigationAgent directed to http://169.254.169.254 (cloud metadata) or internal servicesDomain allowlist blocks non-approved domains
Prompt injection via page contentMalicious page injects instructions into extracted text that hijack agent behaviourExtraction size limits; prefer structured selectors over full-page text
Data exfiltration via screenshotScreenshot captures sensitive data displayed in the browserOperator must review which agents have WebDriver tools assigned
Long-running browser resource drainAgent opens many sessions, exhausting server memorySession limits: MaxIdleSeconds, MaxSessionDurationSeconds, per-tenant session cap
Non-headless browser on serverGUI process fails to start or exposes display serverHeadless: true is enforced; startup fails if set to false in non-dev environments

Domain Allowlist Implementation

public class DomainPolicy
{
    private readonly IReadOnlyList<string> _allowedPatterns;

    public DomainPolicy(WebDriverPluginConfig config)
    {
        _allowedPatterns = config.AllowedDomains;
    }

    public bool IsAllowed(string url)
    {
        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
            return false;

        // Always block localhost and RFC-1918 private ranges
        if (IsPrivateOrLoopback(uri.Host))
            return false;

        // Check against wildcard patterns
        return _allowedPatterns.Any(pattern => MatchesPattern(uri.Host, pattern));
    }

    private static bool MatchesPattern(string host, string pattern)
    {
        if (pattern.StartsWith("*."))
        {
            var suffix = pattern[1..];  // e.g., ".company.com"
            return host.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)
                || host.Equals(suffix[1..], StringComparison.OrdinalIgnoreCase);
        }
        return host.Equals(pattern, StringComparison.OrdinalIgnoreCase);
    }

    private static bool IsPrivateOrLoopback(string host)
    {
        if (host == "localhost" || host == "127.0.0.1" || host == "::1") return true;
        if (IPAddress.TryParse(host, out var ip))
        {
            // Block 10.x.x.x, 172.16.x.x - 172.31.x.x, 192.168.x.x, 169.254.x.x
            var bytes = ip.GetAddressBytes();
            if (bytes[0] == 10) return true;
            if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) return true;
            if (bytes[0] == 192 && bytes[1] == 168) return true;
            if (bytes[0] == 169 && bytes[1] == 254) return true;
        }
        return false;
    }
}

Headless Enforcement

// In OnStartAsync — fail fast if headless is false in non-dev environment
public async Task OnStartAsync(IServiceProvider sp, CancellationToken ct)
{
    var env    = sp.GetRequiredService<IWebHostEnvironment>();
    var config = sp.GetRequiredService<IOptions<WebDriverPluginConfig>>().Value;

    if (!config.Headless && !env.IsDevelopment())
    {
        throw new InvalidOperationException(
            "WebDriverPlugin: Headless must be true outside Development environment. " +
            "Set WebDriverPlugin:Headless to true in appsettings.Production.json.");
    }
}
AllowedDomains must be configured before production deployment. An empty allowlist blocks all navigation. A poorly specified allowlist allows SSRF. Review the allowlist with your security team before deploying agents that use browser tools.

Per-Tenant Session Limits

{
  "WebDriverPlugin": {
    "MaxSessionsPerTenant": 5,
    "MaxGlobalSessions":    50
  }
}

When a tenant's session count reaches MaxSessionsPerTenant, the oldest idle session for that tenant is closed before opening a new one. If the global session cap is reached, new browser_navigate calls return an error until a session expires.