drupal-mcp-connector 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.1] - 2026-06-15
11
+
12
+ ### Security
13
+ - Validate and URL-encode JSON:API path segments. The entity `id` is now checked
14
+ with `validateUuid` and `entityType`/`bundle` with `validateMachineName` (both
15
+ previously unused), and every segment is `encodeURIComponent`'d. This closes a
16
+ path-traversal vector where a crafted `id` (e.g. `../../user/user/<uuid>`) could
17
+ reach a different resource type and bypass the connector's entity-type/PII
18
+ policy. (Drupal core permissions were always still enforced.)
19
+
20
+ ### Added
21
+ - A [Threat Model](docs/threat-model.md) documenting trust boundaries, threats &
22
+ mitigations, residual risks (drush SQL bridge; why filter-field names are not
23
+ machine-name-validated), and the 1.0 security-pass results (`npm audit` clean,
24
+ adversarial review).
25
+
10
26
  ## [0.9.0] - 2026-06-15
11
27
 
12
28
  ### Added
@@ -213,6 +229,7 @@ The connector is now **dual-protocol**: every tool runs against an abstract back
213
229
  - User tools gained explicit PII-access assertions.
214
230
  - Whole tree lint-clean (`npm run lint`) with object-injection sinks rewritten to safe lookups.
215
231
 
232
+ [0.9.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v0.9.1
216
233
  [0.9.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v0.9.0
217
234
  [0.8.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v0.8.0
218
235
  [0.7.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v0.7.1
package/README.md CHANGED
@@ -177,6 +177,7 @@ Governance keys off the authenticated account's role and OAuth scopes — not re
177
177
  | [Tools Reference](docs/tools-reference.md) | Full reference for all 66 tools |
178
178
  | [Security Guide](docs/security.md) | Presets, entity access control, field redaction |
179
179
  | [Security Hardening](docs/security-hardening.md) | Optional transport, identity, and secrets controls |
180
+ | [Threat Model](docs/threat-model.md) | Trust boundaries, threats & mitigations, residual risks, and the security-pass results |
180
181
  | [Deployment](docs/deployment.md) | Run the HTTPS transport in production: Docker, systemd, launchd, reverse proxy, pre-exposure checklist |
181
182
  | [Integration Contract](docs/integration-contract.md) | The connector ↔ Drupal-governance contract (identity, OAuth scopes, compatibility) |
182
183
  | [Versioning & Stability](docs/versioning.md) | Semver policy: the stable surface, deprecation process, MCP protocol + Node support |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drupal-mcp-connector",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "A secure, multi-site Model Context Protocol (MCP) connector for Drupal — dual-protocol JSON:API and GraphQL.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { drupalFetch, drupalUploadFile } from "../drupal-fetch.js";
11
+ import { validateUuid, validateMachineName } from "../validate.js";
11
12
  import { Backend } from "./backend-interface.js";
12
13
  import {
13
14
  makeCanonicalEntity,
@@ -132,7 +133,12 @@ export class JsonApiBackend extends Backend {
132
133
  * @returns {string} e.g. "/jsonapi/node/article".
133
134
  */
134
135
  resourcePath(entityType, bundle) {
135
- return `/jsonapi/${entityType}/${bundle}`;
136
+ // Validate before interpolating into the URL: machine names cannot contain
137
+ // path separators or `..`, so this blocks path-traversal to other resources
138
+ // (e.g. id="../../user/user/…"). Encoding is belt-and-suspenders.
139
+ validateMachineName(entityType, "entityType");
140
+ validateMachineName(bundle, "bundle");
141
+ return `/jsonapi/${encodeURIComponent(entityType)}/${encodeURIComponent(bundle)}`;
136
142
  }
137
143
 
138
144
  /**
@@ -219,7 +225,8 @@ export class JsonApiBackend extends Backend {
219
225
  * @returns {Promise<?import("../canonical.js").CanonicalEntity>} Entity, or null.
220
226
  */
221
227
  async getEntity({ entityType, bundle, id }) {
222
- const data = await drupalFetch(this.site, `${this.resourcePath(entityType, bundle)}/${id}`);
228
+ validateUuid(id);
229
+ const data = await drupalFetch(this.site, `${this.resourcePath(entityType, bundle)}/${encodeURIComponent(id)}`);
223
230
  return data?.data ? this.toCanonical(data.data) : null;
224
231
  }
225
232
 
@@ -275,12 +282,13 @@ export class JsonApiBackend extends Backend {
275
282
  * @returns {Promise<import("../canonical.js").CanonicalEntity>} The updated entity.
276
283
  */
277
284
  async updateEntity({ entityType, bundle, id, attributes = {}, relationships }) {
285
+ validateUuid(id);
278
286
  const buildPayload = (attrs) => {
279
287
  const payload = { data: { type: `${entityType}--${bundle}`, id, attributes: attrs } };
280
288
  if (relationships) payload.data.relationships = relationships;
281
289
  return payload;
282
290
  };
283
- const data = await this.writeWithModerationFallback(`${this.resourcePath(entityType, bundle)}/${id}`, "PATCH", buildPayload, attributes);
291
+ const data = await this.writeWithModerationFallback(`${this.resourcePath(entityType, bundle)}/${encodeURIComponent(id)}`, "PATCH", buildPayload, attributes);
284
292
  return this.toCanonical(data.data);
285
293
  }
286
294
 
@@ -290,7 +298,8 @@ export class JsonApiBackend extends Backend {
290
298
  * @returns {Promise<void>}
291
299
  */
292
300
  async deleteEntity({ entityType, bundle, id }) {
293
- await drupalFetch(this.site, `${this.resourcePath(entityType, bundle)}/${id}`, { method: "DELETE" });
301
+ validateUuid(id);
302
+ await drupalFetch(this.site, `${this.resourcePath(entityType, bundle)}/${encodeURIComponent(id)}`, { method: "DELETE" });
294
303
  }
295
304
 
296
305
  /**