BACK TO SEARCH
GetSomeKelso/OhSINTcritical

Unified OSINT reconnaissance orchestrator — 28 tools behind a single CLI + MCP server (Kali VM → Claude Desktop/Code)

OhSINT is a unified OSINT reconnaissance orchestrator that wraps 73 tools behind a single CLI and MCP server, enabling automated intelligence gatherin...

purpose: OhSINT is a unified OSINT reconnaissance orchestrathreat: network exposed
Python · 0 · May 29, 2026 · May 30, 2026 · GITHUB ↗
RISK SCORE
0/ 100 risk
high findings+225
medium findings+60
capped at100
VULNERABILITY ANALYSIS · 13 findings in 13 blocks9 HIGH · 4 MEDIUM
HIGH1 finding
src/mcp/server.py:674
674@mcp.tool()
675async def osint_report(
676    results_dir: str,
677    output_format: str = "all",
678) -> str:
679    """Generate a consolidated report from scan results in a directory."""
680    from src.report import load_report
681
682    path = Path(results_dir)
683    json_file = path / "report.json"
684    if not json_file.exists():
685        return f"No report.json found in {results_dir}"
686
687    report = load_report(json_file)
688    save_report(report, path, output_format)
689    return report.to_markdown()
src/mcp/server.py:674

// Network-exposed MCP server; no authorization required for this tool.

EXPLAINThe osint_report tool accepts an arbitrary path via the results_dir parameter without any validation or sanitization. It constructs a Path object from user input and reads report.json from that directory. An attacker can specify any directory on the filesystem (e.g., /etc, /home/user/.ssh) and if a report.json exists there, its contents will be read and returned. Even if report.json doesn't exist, the error message reveals whether the directory exists. This tool is exposed without requiring authorization_confirmed, making it accessible to any MCP client.
IMPACTAn attacker could read arbitrary files named report.json from any directory the server process has access to, potentially leaking sensitive information such as SSH keys, configuration files, or other OSINT results stored outside the intended results directory.
FIXRestrict results_dir to a specific allowed base directory (e.g., DEFAULT_RESULTS_DIR) and validate that the resolved path is within that base. Alternatively, require authorization_confirmed for this tool.
HIGH1 finding
src/mcp/server.py:294
294@mcp.tool()
295async def osint_exiftool(
296    directory: str,
297    filter_fields: str = "Author|Creator|Email|Producer|Template|Software",
298    authorization_confirmed: bool = False,
299) -> str:
300    """Extract metadata from downloaded files in a directory."""
301    return await _run_tool_audited(
302        "exiftool", directory, authorization_confirmed,
303        filter_fields=filter_fields,
304    )
src/mcp/server.py:294

// Network-exposed MCP server; tool is passive and does not require authorization.

EXPLAINThe osint_exiftool tool accepts an arbitrary directory path without validation. The directory parameter is passed directly to the underlying tool, which will read files from that directory and extract metadata. An attacker can specify any directory (e.g., /etc, /home/user) to extract metadata from files there, potentially leaking sensitive information embedded in file metadata (authors, emails, software versions). The tool is marked as passive and does not require authorization_confirmed.
IMPACTAn attacker could read metadata from arbitrary files on the server, potentially extracting sensitive information such as usernames, email addresses, software versions, and other metadata from configuration files, documents, or any other files in accessible directories.
FIXRestrict the directory parameter to a specific allowed base directory (e.g., DEFAULT_RESULTS_DIR) and validate that the resolved path is within that base. Alternatively, require authorization_confirmed for this tool.
HIGH1 finding
src/mcp/server.py:307
307@mcp.tool()
308async def osint_github_dorks(
309    target: str,
310    dork_file: str = "",
311    authorization_confirmed: bool = False,
312) -> str:
313    """Scan GitHub repos/orgs for sensitive information leaks."""
314    kwargs: dict[str, Any] = {}
315    if dork_file:
316        kwargs["dork_file"] = dork_file
317    return await _run_tool_audited(
318        "github_dorks", target, authorization_confirmed, **kwargs,
319    )
src/mcp/server.py:307

// Network-exposed MCP server; no authorization required for this tool.

EXPLAINThe osint_github_dorks tool accepts an arbitrary file path via the dork_file parameter. This path is passed to the underlying tool, which will read the file and use its contents as dork queries. An attacker can specify any file on the filesystem (e.g., /etc/passwd, /home/user/.ssh/id_rsa) and the tool will read and process it, potentially leaking its contents through error messages or output. The tool does not require authorization_confirmed.
IMPACTAn attacker could read arbitrary files from the server by specifying their paths as dork_file, potentially leaking sensitive information such as SSH keys, configuration files, or credentials.
FIXRestrict dork_file to a specific allowed directory (e.g., a dorks directory) and validate that the resolved path is within that base. Alternatively, require authorization_confirmed for this tool.
HIGH1 finding
src/mcp/server.py:1057
1057@mcp.tool()
1058async def osint_js_analysis(
1059    domains: str,
1060    js_urls_file: str = "",
1061    authorization_confirmed: bool = False,
1062) -> str:
1063    """Analyze JavaScript files for endpoints, secrets, and source maps."""
1064    ...
1065    report = await asyncio.to_thread(
1066        pipeline.run, domain_list, js_urls_file or None, output_dir,
1067    )
src/mcp/server.py:1057

// Network-exposed MCP server; no authorization required for this tool.

EXPLAINThe osint_js_analysis tool accepts an arbitrary file path via the js_urls_file parameter. This path is passed to the pipeline, which will read the file and parse its contents as a list of JavaScript URLs. An attacker can specify any file on the filesystem, and the tool will attempt to read and parse it, potentially leaking its contents through error messages or processing. The tool does not require authorization_confirmed.
IMPACTAn attacker could read arbitrary files from the server by specifying their paths as js_urls_file, potentially leaking sensitive information.
FIXRestrict js_urls_file to a specific allowed directory (e.g., within DEFAULT_RESULTS_DIR) and validate that the resolved path is within that base. Alternatively, require authorization_confirmed for this tool.
HIGH1 finding
src/mcp/server.py:863
863@mcp.tool()
864async def osint_subdomain_takeover(
865    domains: str,
866    re_scan_path: str = "",
867    authorization_confirmed: bool = False,
868    per_domain: bool = False,
869    max_subdomains: int = 5000,
870) -> str:
871    ...
872    re_path = Path(re_scan_path) if re_scan_path else None
873    ...
874    report = await asyncio.to_thread(
875        pipeline.run, domain_list, re_path, max_subdomains,
876    )
src/mcp/server.py:863

// Network-exposed MCP server; requires authorization_confirmed but still allows arbitrary file read.

EXPLAINThe osint_subdomain_takeover tool accepts an arbitrary file path via the re_scan_path parameter. This path is converted to a Path object and passed to the pipeline, which will read the file as a previous report.json. An attacker can specify any file on the filesystem, and the tool will attempt to read and parse it, potentially leaking its contents through error messages or processing. Although this tool requires authorization_confirmed, the path traversal still allows reading arbitrary files once authorized.
IMPACTAn attacker with authorization could read arbitrary files from the server by specifying their paths as re_scan_path, potentially leaking sensitive information.
FIXRestrict re_scan_path to a specific allowed directory (e.g., within DEFAULT_RESULTS_DIR) and validate that the resolved path is within that base.
HIGH1 finding
src/cli.py:548
548@click.option("--scope-file", type=click.Path(exists=True), default=None,
549              help="HackerOne/Bugcrowd scope file — parses assets, prompts for selection.")
550...
551def url_harvest(ctx, target, targets_file, scope_file, max_urls):
552    ...
553    domains = _resolve_takeover_targets(ctx, target, targets_file, scope_file)
554    ...
555    report = pipeline.run(targets=domains, max_urls=max_urls, output_dir=output_dir)
src/cli.py:548

// Local-only MCP, requires compromised LLM to exploit; but still a finding.

EXPLAINMultiple CLI commands (url-harvest, secret-surface, js-analysis, passive-full) accept an arbitrary file path via the --scope-file option. The click.Path(exists=True) validator only checks that the file exists, not that it's within an allowed directory. The file is parsed by parse_scope_file, which reads and processes its contents. An attacker with access to the CLI could specify any file on the filesystem, and the tool will read and parse it, potentially leaking its contents through error messages or processing.
IMPACTAn attacker could read arbitrary files from the server by specifying their paths as --scope-file, potentially leaking sensitive information.
FIXRestrict --scope-file to a specific allowed directory and validate that the resolved path is within that base.
HIGH1 finding
src/cli.py:356
356@click.option("--re-scan", type=click.Path(exists=True), default=None,
357              help="Previous report.json — skip enumeration, re-run fingerprinting only.")
358...
359re_scan_path = Path(re_scan) if re_scan else None
360...
361report = pipeline.run(
362    targets=domains,
363    re_scan_path=re_scan_path,
364    ...
365)
src/cli.py:356

// Local-only MCP, requires compromised LLM to exploit; but still a finding.

EXPLAINThe CLI takeover command accepts an arbitrary file path via the --re-scan option. The click.Path(exists=True) validator only checks that the file exists, not that it's within an allowed directory. An attacker with access to the CLI (e.g., via a compromised LLM that can execute shell commands) could specify any file on the filesystem, and the tool will read and parse it, potentially leaking its contents.
IMPACTAn attacker could read arbitrary files from the server by specifying their paths as --re-scan, potentially leaking sensitive information.
FIXRestrict --re-scan to a specific allowed directory (e.g., within DEFAULT_RESULTS_DIR) and validate that the resolved path is within that base.
HIGH1 finding
src/cli.py:548
548@click.option("--targets-file", type=click.Path(exists=True), default=None,
549              help="File with one domain per line.")
550...
551def url_harvest(ctx, target, targets_file, scope_file, max_urls):
552    ...
553    domains = _resolve_takeover_targets(ctx, target, targets_file, scope_file)
554    ...
555    report = pipeline.run(targets=domains, max_urls=max_urls, output_dir=output_dir)
src/cli.py:548

// Local-only MCP, requires compromised LLM to exploit; but still a finding.

EXPLAINMultiple CLI commands accept an arbitrary file path via the --targets-file option. The click.Path(exists=True) validator only checks that the file exists, not that it's within an allowed directory. The file is read and parsed as a list of targets. An attacker with access to the CLI could specify any file on the filesystem, and the tool will read and parse it, potentially leaking its contents through error messages or processing.
IMPACTAn attacker could read arbitrary files from the server by specifying their paths as --targets-file, potentially leaking sensitive information.
FIXRestrict --targets-file to a specific allowed directory and validate that the resolved path is within that base.
HIGH1 finding
src/cli.py:627
627@click.option("--js-urls-file", type=click.Path(exists=True), default=None,
628              help="JS URL list from url-harvest pipeline (js_urls.txt).")
629...
630def js_analysis(ctx, target, targets_file, scope_file, js_urls_file):
631    ...
632    report = pipeline.run(targets=domains, js_urls_file=js_urls_file, output_dir=output_dir)
src/cli.py:627

// Local-only MCP, requires compromised LLM to exploit; but still a finding.

EXPLAINThe CLI js-analysis command accepts an arbitrary file path via the --js-urls-file option. The click.Path(exists=True) validator only checks that the file exists, not that it's within an allowed directory. The file is read and parsed as a list of JavaScript URLs. An attacker with access to the CLI could specify any file on the filesystem, and the tool will read and parse it, potentially leaking its contents.
IMPACTAn attacker could read arbitrary files from the server by specifying their paths as --js-urls-file, potentially leaking sensitive information.
FIXRestrict --js-urls-file to a specific allowed directory and validate that the resolved path is within that base.
MEDIUM1 finding
src/config.py:36
36class Config:
37    def __init__(
38        self,
39        api_keys_path: Optional[Path] = None,
40        profiles_path: Optional[Path] = None,
41    ):
42        self.api_keys_path = api_keys_path or DEFAULT_API_KEYS_FILE
43        ...
44    def _load(self) -> None:
45        if self.api_keys_path.exists():
46            with open(self.api_keys_path) as f:
47                self._api_keys = yaml.safe_load(f) or {}
src/config.py:36

// Network-exposed MCP server; combined with file read vulnerabilities, this becomes critical.

EXPLAINAPI keys are stored in a plaintext YAML file (configs/api_keys.yaml) and loaded into memory. While this is a common pattern, the file contains sensitive credentials for multiple services (Shodan, VirusTotal, Censys, Twilio, etc.). If an attacker gains access to the filesystem (e.g., through the arbitrary file read vulnerabilities), they can extract all API keys. Additionally, the keys are held in memory for the lifetime of the process.
IMPACTAn attacker who gains access to the filesystem or memory could extract all configured API keys, leading to unauthorized usage of third-party services, potential financial costs, and data breaches.
FIXUse a secrets management solution (e.g., environment variables, encrypted vault) instead of plaintext files. Consider encrypting the API keys file at rest.
MEDIUM1 finding
src/mcp/server.py:1261
1261def main():
1262    parser.add_argument("--host", default="127.0.0.1",
1263                        help="Bind address (default: 127.0.0.1, use 0.0.0.0 for Hyper-V)")
1264    ...
1265    if token:
1266        logger.info("Bearer token authentication ENABLED")
1267    else:
1268        logger.warning(
1269            "No bearer token configured — MCP server is UNAUTHENTICATED. "
1270            "Set --token, OHSINT_MCP_TOKEN, or mcp_server.bearer_token in api_keys.yaml"
1271        )
src/mcp/server.py:1261

// Network-exposed MCP server; default configuration may be unauthenticated.

EXPLAINThe MCP server can be started with --host 0.0.0.0 to bind to all network interfaces, and if no bearer token is configured, the server runs completely unauthenticated. While the default is 127.0.0.1, the documentation explicitly suggests using 0.0.0.0 for Hyper-V scenarios, and the warning about missing token is just a log message, not an enforcement. This allows anyone on the network to access all OSINT tools without authentication.
IMPACTAn attacker on the same network could access the MCP server and execute all OSINT tools, including active reconnaissance tools, without any authentication. This could lead to unauthorized scanning of targets, data exfiltration, and potential legal liability.
FIXRequire authentication by default when binding to non-localhost addresses. Consider making the token mandatory when host is not 127.0.0.1.
MEDIUM1 finding
src/mcp/server.py:163
163def _require_auth(authorization_confirmed: bool, tool_name: str = "") -> None:
164    if tool_name:
165        tool = _get_orchestrator().get_tool(tool_name)
166        if tool and tool.is_passive:
167            return  # passive tools don't need authorization
168    if not authorization_confirmed:
169        raise ValueError(
170            "Authorization not confirmed. Active reconnaissance tools require "
171            "WRITTEN AUTHORIZATION from the target owner before execution."
172        )
src/mcp/server.py:163

// Network-exposed MCP server; passive tools are accessible without authorization.

EXPLAINThe authorization gate is bypassed for all tools marked as 'passive'. However, many passive tools (e.g., osint_theharvester, osint_spiderfoot, osint_shodan, osint_censys, osint_intelx, osint_hudson_rock) can still gather significant intelligence about a target without interacting with it directly. The distinction between passive and active is based on the tool's classification, not on the sensitivity of the data collected. An attacker could use passive tools to gather emails, subdomains, breached credentials, and other sensitive information without any authorization check.
IMPACTAn attacker could use passive OSINT tools to gather sensitive information about any target without authorization, potentially violating terms of service of third-party APIs and enabling further attacks.
FIXConsider requiring authorization for all tools that collect data about a specific target, regardless of whether they are classified as passive. Alternatively, implement a separate authorization gate for passive tools that collect sensitive data.
MEDIUM1 finding
src/mcp/server.py:183
183@mcp.tool()
184async def osint_full_recon(
185    target: str,
186    profile: str = "passive",
187    authorization_confirmed: bool = False,
188    output_format: str = "all",
189    timeout: int = DEFAULT_TIMEOUT,
190) -> str:
191    """Run full OSINT reconnaissance against a target using a scan profile."""
192    _require_auth(authorization_confirmed)
193    start = _time.time()
194    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
195    safe_target = target.replace("/", "_").replace(":", "_")
196    output_dir = DEFAULT_RESULTS_DIR / safe_target / timestamp
src/mcp/server.py:183

// Network-exposed MCP server; input validation is missing across multiple tools.

EXPLAINThe target parameter in osint_full_recon and many other tools is not validated for format or content. While the code does replace '/' and ':' with '_' for directory creation, the target is passed directly to underlying tools without validation. This could allow injection of special characters or commands depending on how the target is used by the underlying tool. Additionally, the target is used in audit logs and output, potentially enabling log injection.
IMPACTAn attacker could inject special characters or commands through the target parameter, potentially leading to command injection in underlying tools or log injection in audit logs.
FIXValidate target parameters against expected formats (domain, email, IP, etc.) and sanitize inputs before passing to tools or logging.
5/30/2026
Findings are produced by automated LLM analysis and may include false positives or miss issues. Verify independently before acting.