Back to skill

Security audit

ArcGIS Agent

Security checks for vulnerabilities and agentic risk

Overview

The skill is purpose-aligned for ArcGIS automation, but it exposes very powerful local GIS operations through an unauthenticated localhost API with weak guardrails.

Install only if you are comfortable running a local ArcGIS automation server that can change GIS files and possibly use active ArcGIS portal credentials. Restrict exposed modules before use, run it only while needed, keep it bound to localhost, use a tightly controlled workspace, avoid active portal sessions unless required, and confirm any write, overwrite, edit, delete-like, publish, or long-running operation explicitly.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/server.py:110
Finding
Path Allowlist Bypass Permits Operations Outside Approved Directories<![CDATA[ ## Vulnerability Details **File Location**: `scripts/server.py:110-116`, `scripts/server.py:150-154`, and `scripts/server.py:177-182` **Vulnerability Type**: Incomplete path validation and allowlist bypass **Risk Level**: High ### Vulnerable Code ```python ALLOWED_PATHS = [p for p in os.environ.get("ARCPY_ALLOWED_PATHS", "C:/GIS-AI-Course/;C:\\GIS-AI-Course\\").split(";") if p.strip()] def check_path(p): if not p or not isinstance(p, str): return True p = p.replace("\\", "/").lower() return any(p.startswith(a.replace("\\", "/").lower()) for a in ALLOWED_PATHS) ``` ```python # Path security check for k, v in args.items(): if isinstance(v, str) and ("/" in v or "\\" in v) and (".shp" in v.lower() or ".tif" in v.lower() or ".gdb" in v.lower()): if not check_path(v): self.json({"status": "error", "message": f"Path not allowed: {v}"}, 403) return ``` ```python # Parse kwargs: handle both flat args and JSON string kwargs if "__kwargs" in args and len(args) == 1: kwargs = json.loads(args["__kwargs"]) else: kwargs = {k: v for k, v in args.items() if not k.startswith("__")} # Execute! result = func(**kwargs) ``` ### Technical Analysis The server claims to enforce a path allowlist, but the validation is shallow and based on string heuristics rather than canonical filesystem paths. The implementation has several bypass conditions: 1. **Nested values are not inspected.** Only top-level string values in `args` are validated. Lists, dictionaries, and other nested structures are ignored. ArcPy tools commonly accept lists of input datasets, such as the `in_features` argument for intersection and merge operations. 2. **Only three path patterns trigger validation.** A string is checked only if it contains `.shp`, `.tif`, or `.gdb`. ArcPy supports many additional path-bearing resource types, including CSV files, text files, XML files, geodatabases using other formats, database connections, layer ...[truncated 2514 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Construct the final `kwargs` first and recursively inspect every list, tuple, dictionary, and string before invoking ArcPy. 2. Canonicalize local paths with `pathlib.Path.resolve(strict=False)` or an equivalent Windows-aware mechanism. 3. Verify containment using `os.path.commonpath` or resolved `Path` parents rather than `startswith`. 4. Reject traversal components, ambiguous relative paths, device paths, UNC paths, alternate data streams, and unsupported URI schemes unless explicitly required. 5. Require absolute paths and resolve them against a controlled base directory. 6. Obtain ArcPy parameter metadata and validate every path-bearing parameter instead of detecting paths through three filename extensions. 7. Maintain separate read and write allowlists. Output paths should receive stricter validation than input paths. 8. Revalidate every path immediately before tool execution, including paths embedded in composite parameters. 9. Normalize administrator-supplied allowed roots and enforce directory-boundary semantics. 10. Add regression tests for nested lists, dictionaries, traversal paths, sibling-prefix paths, relative paths, mixed separators, case variations, UNC paths, and all supported dataset formats. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/server.py:141
Finding
Unauthenticated Local API Executes Powerful ArcPy Tools<![CDATA[ ## Vulnerability Details **File Location**: `scripts/server.py:119-120`, `scripts/server.py:141-184`, `scripts/server.py:224-228`, and `scripts/server.py:235-236` **Vulnerability Type**: Missing authentication and authorization on a privileged tool-execution endpoint **Risk Level**: High ### Vulnerable Code ```python class MCP(http.server.BaseHTTPRequestHandler): def do_GET(self): ``` ```python def do_POST(self): if self.path != "/call": self.json({"error": "not found"}, 404) return try: body = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0)))) tool_id = body.get("name", "") args = body.get("arguments", {}) # Path security check for k, v in args.items(): if isinstance(v, str) and ("/" in v or "\\" in v) and (".shp" in v.lower() or ".tif" in v.lower() or ".gdb" in v.lower()): if not check_path(v): self.json({"status": "error", "message": f"Path not allowed: {v}"}, 403) return tdef = TOOL_REGISTRY.get(tool_id) if not tdef: self.json({"status": "error", "message": f"Tool not found: {tool_id}. Call /tools to list available tools."}, 404) return arcpy.env.overwriteOutput = True t0 = time.time() mod = getattr(arcpy, tdef["module"]) func = getattr(mod, tdef["func"]) # Parse kwargs: handle both flat args and JSON string kwargs if "__kwargs" in args and len(args) == 1: kwargs = json.loads(args["__kwargs"]) else: kwargs = {k: v for k, v in args.items() if not k.startswith("__")} # Execute! result = func(**kwargs) ``` ```python def json(self, data, code=200): self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(data, ensu ...[truncated 3875 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate a high-entropy authentication token at startup and require it in an authorization header for every endpoint except, if necessary, a minimal health check. 2. Store and compare the token securely; do not expose it through logs, health responses, or tool metadata. 3. Validate `Origin` and `Host` headers against explicit localhost values. Reject unexpected origins rather than returning wildcard CORS headers. 4. Remove `Access-Control-Allow-Origin: *`. If browser access is required, allow only an explicitly configured trusted origin. 5. Require `Content-Type: application/json` for `/call` and reject simple browser content types. 6. Introduce a default-deny tool allowlist. Expose only the minimum tools needed for the deployment. 7. Disable high-risk modules such as `management`, `edit`, `server`, and `sharing` unless explicitly enabled. 8. Classify tools as read-only, write, destructive, publishing, or network-capable and enforce separate authorization policies. 9. Require short-lived approval or capability tokens for destructive and externally visible operations. 10. Do not rely on agent instructions for confirmation; enforce confirmation state in the server. 11. Consider replacing the HTTP interface with authenticated operating-system IPC when only the local MCP bridge needs access. 12. Add request-size limits, execution timeouts, concurrency controls, rate limits, and cancellation support to reduce denial-of-service risk. 13. Run the service under a dedicated least-privileged account with access only to approved GIS workspaces and without unnecessary portal credentials. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (18)

Tool Parameter Abuse

High
Category
Tool Misuse
Content
| indoors / indoorpositioning | 22/8 | Indoor GIS / positioning | Indoors |
| maritime / bathymetry | 22/8 | S-57 charts, bathymetry | Maritime |
| ca / td / intelligence | 13/19/18 | Crime analysis / territory design / intelligence | respective ext. |
| rm / oi / reviewer / wmx / transit / geoai | 14/6/5/18/8/12 | Ortho mapping / oriented imagery / QA / workflow / GTFS / GeoAI | respective ext. |

**Portal / Enterprise modules** (calls require an active Portal or ArcGIS Online sign-in): `ra`, `geoanalytics`, `gapro`, `sfa`, `agolservices`
Confidence
85% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
| indoors / indoorpositioning | 22/8 | Indoor GIS / positioning | Indoors |
| maritime / bathymetry | 22/8 | S-57 charts, bathymetry | Maritime |
| ca / td / intelligence | 13/19/18 | Crime analysis / territory design / intelligence | respective ext. |
| rm / oi / reviewer / wmx / transit / geoai | 14/6/5/18/8/12 | Ortho mapping / oriented imagery / QA / workflow / GTFS / GeoAI | respective ext. |

**Portal / Enterprise modules** (calls require an active Portal or ArcGIS Online sign-in): `ra`, `geoanalytics`, `gapro`, `sfa`, `agolservices`
Confidence
85% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill exposes network and environment-dependent behavior but does not declare an explicit tool scope such as allowed-tools or permissions. That omission weakens least-privilege controls and can cause the agent to activate capabilities beyond what a reviewer or orchestrator expects, especially given the documented ability to start a local server and modify GIS datasets.

Vague Triggers

Medium
Confidence
88% confidence
Finding
The trigger text is very broad: it activates for general GIS, ArcGIS automation, spatial analysis, and arcpy requests without strong boundary conditions. Broad activation increases the chance this skill is invoked in contexts where users did not intend local server calls or file-modifying geoprocessing actions, creating an unsafe prompt-to-action pathway.

External Transmission

Medium
Category
Data Exfiltration
Content
### Step 1 — make sure the server is running

```bash
curl -s http://127.0.0.1:8765/health
# {"status":"ok","server":"arcpy-mcp-server","version":"2.1","tools":2500,"modules":46}
```
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill documents write-capable operations and notes overwrite behavior, but the warning is not prominent or general enough for destructive actions. In a workflow that can create, replace, edit, publish, or transform GIS datasets, lack of a strong upfront warning can lead to unintended data loss or silent modification of authoritative spatial data.

External Transmission

Medium
Category
Data Exfiltration
Content
### 第一步:确保服务在运行

```bash
curl -s http://127.0.0.1:8765/health
# {"status":"ok","server":"arcpy-mcp-server","version":"2.1","tools":2500,"modules":46}
```
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
This example performs in-place modification of the source dataset by adding a field and calculating values directly on buildings.shp. In an agent context, examples that normalize direct mutation without warning can lead users or downstream implementations to alter authoritative GIS data unintentionally, causing data integrity loss and difficult-to-reverse changes.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
This code transmits the incoming tool name and arguments over HTTP via POST to the local MCP server. While the action is visible in code, there is no confirmation prompt, user-facing print/log message about forwarding request data, or comment/docstring warning that user-provided arguments are sent over HTTP.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
The server exposes geoprocessing execution over HTTP and explicitly sets arcpy.env.overwriteOutput = True before calling the selected tool. Many ArcGIS tools can overwrite, delete, transform, publish, or otherwise modify local and enterprise data, and the code provides no authentication, confirmation, dry-run mode, or per-tool safeguards; the path check is narrow and bypassable for non-matching destructive operations.

Natural-Language Policy Violations

Low
Confidence
91% confidence
Finding
All headings, user utterances, and AI responses are presented exclusively in Chinese, with no indication that users may interact in other languages or that the skill is intentionally limited to a Chinese-only context. Per SQP-3, forcing a specific language without user opt-in is a natural-language policy concern.

Missing User Warnings

Low
Confidence
84% confidence
Finding
This markdown file includes an 'AI 自动执行' workflow that creates multiple output files such as buffer, slope, reclass, and candidate datasets, but it does not warn the user that the skill will write results to disk. For markdown files, SQP-2 applies when descriptions omit warnings about behaviors that could affect user data or system state.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The comment on line 3 is written in Chinese, which imposes a specific language in the file's natural-language guidance without any indication of user opt-in or documented locale scope. Under the stated policy, language-specific instructions should either be optional or clearly justified as region-specific.

Natural-Language Policy Violations

Low
Confidence
87% confidence
Finding
The module docstring is written entirely in Chinese and does not indicate that language choice is optional or that the skill is intentionally region-specific. Under the stated policy, forcing a specific language without user opt-in is a locale-policy concern.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
continue
    if mod_name not in INCLUDE_MODULES:
        continue
    m = getattr(arcpy, mod_name)
    tools = [t for t in dir(m) if not t.startswith('_') and t[0].isupper()]
    
    for func_name in tools:
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
for func_name in tools:
        tool_id = f"{mod_name}_{func_name}"
        try:
            func = getattr(m, func_name)
            # Basic schema: just accept **kwargs
            TOOL_REGISTRY[tool_id] = {
                "module": mod_name,
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
arcpy.env.overwriteOutput = True
            t0 = time.time()
            
            mod = getattr(arcpy, tdef["module"])
            func = getattr(mod, tdef["func"])
            
            # Parse kwargs: handle both flat args and JSON string kwargs
Confidence
89% confidence
Finding
This dynamic module lookup occurs in the HTTP request execution path and is part of exposing a very large set of powerful ArcGIS operations over localhost. While tdef is sourced from an internal registry, that registry is auto-built from many modules and enables remote invocation of sensitive filesystem- and data-modifying functionality without meaningful per-tool authorization or risk gating.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
t0 = time.time()
            
            mod = getattr(arcpy, tdef["module"])
            func = getattr(mod, tdef["func"])
            
            # Parse kwargs: handle both flat args and JSON string kwargs
            if "__kwargs" in args and len(args) == 1:
Confidence
90% confidence
Finding
This line dynamically resolves and executes the requested ArcGIS function from HTTP-controlled tool selection. In context, it is the sink for an unauthenticated local API that can invoke destructive geoprocessing actions, making the reflection dangerous because it operationalizes arbitrary registry-exposed tool execution.

Static analysis

No suspicious patterns detected.