@mindstone/mcp-server-microsoft-teams 0.3.0 → 0.3.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
@@ -4,6 +4,16 @@ All notable changes to this connector will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.3.1] - 2026-08-10
8
+
9
+ ### Changed
10
+
11
+ - Re-sync the vendored envelope helper with the hardened canonical: attribute-bearing close tags and spoofed open tags escaped.
12
+
13
+ ### Fixed
14
+
15
+ - Re-synced the vendored `<untrusted-content>` envelope helper with the canonical hardened reference: attribute-bearing close-tag variants (`</untrusted-content foo>`) and spoofed open tags inside wrapped content are now escaped, closing an envelope-breakout gap an LLM parser could read as an envelope boundary.
16
+
7
17
  ## [0.3.0] - 2026-08-07
8
18
 
9
19
  ### Changed
package/README.md CHANGED
@@ -9,7 +9,7 @@ Microsoft 365 Teams MCP server — list and read Teams chats and channel message
9
9
 
10
10
  ## Status
11
11
 
12
- - **Version:** [0.3.0](./CHANGELOG.md) · [npm](https://www.npmjs.com/package/@mindstone/mcp-server-microsoft-teams)
12
+ - **Version:** [0.3.1](./CHANGELOG.md) · [npm](https://www.npmjs.com/package/@mindstone/mcp-server-microsoft-teams)
13
13
  - **Auth:** OAuth (host-orchestrated, shared with [`mcp-server-microsoft-mail`](../microsoft-mail/)) ([`MS_CLIENT_ID`](./server.json))
14
14
  - **Tools:** [17](./src/tools.ts) (chats, messages, teams, channels, users, search, presence)
15
15
  - **Surface:** cloud-api
@@ -20,15 +20,15 @@
20
20
  */
21
21
  /**
22
22
  * Wrap a single untrusted string in an `<untrusted-content source="…">`
23
- * envelope, escaping any embedded close-tag variant so the envelope cannot be
24
- * broken out of.
23
+ * envelope, escaping any embedded open/close-tag variant so the envelope can
24
+ * neither be broken out of nor have a spoofed nested envelope planted inside.
25
25
  *
26
26
  * `undefined` and `null` are passed through untouched so callers can apply the wrapper
27
27
  * uniformly to optional fields without branching.
28
28
  *
29
29
  * Idempotent: when `text` is already a properly-shaped envelope for the SAME
30
30
  * `source` (starts with the matching OPEN tag, ends with CLOSE, and contains no
31
- * internal close-tag variants), the original string is returned unchanged so
31
+ * internal open/close-tag variants), the original string is returned unchanged so
32
32
  * `wrapUntrusted(wrapUntrusted(s, src), src) === wrapUntrusted(s, src)`.
33
33
  */
34
34
  export declare function wrapUntrusted(text: string | null | undefined, source: string): string | undefined;
@@ -18,34 +18,44 @@
18
18
  * `untrusted-content` in any connector that talks to an external system; this
19
19
  * file (and the call sites that import from it) is what satisfies that gate.
20
20
  */
21
- const UNTRUSTED_CLOSE_TAG_VARIANT = /<\/untrusted-content\s*>/gi;
21
+ const UNTRUSTED_CLOSE_TAG_VARIANT = /<\/untrusted-content(?:\s[^>]*)?>/gi;
22
22
  const ESCAPED_UNTRUSTED_CLOSE_TAG = '<\\/untrusted-content>';
23
+ const UNTRUSTED_OPEN_TAG_VARIANT = /<untrusted-content(?:\s[^>]*)?>/gi;
24
+ const ESCAPED_UNTRUSTED_OPEN_TAG = '<\\untrusted-content>';
23
25
  const UNTRUSTED_ENVELOPE = /^<untrusted-content source="[^"]*">([\s\S]*)<\/untrusted-content>$/;
24
26
  function escapeAttr(s) {
25
27
  return s.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
26
28
  }
27
29
  /**
28
- * Rewrite every `</untrusted-content>` variant (case-insensitive, optional
29
- * whitespace before `>`) inside `s` to a benign textual form, so an attacker
30
- * who controls the wrapped content cannot terminate the envelope early.
30
+ * Rewrite every `</untrusted-content>` and `<untrusted-content …>` variant
31
+ * (case-insensitive, optional whitespace / attributes before `>`) inside `s`
32
+ * to a benign textual form, so an attacker who controls the wrapped content
33
+ * can neither terminate the envelope early nor spoof a nested open tag that a
34
+ * downstream parser could re-read as a fresh envelope. An LLM parsing the
35
+ * markup plausibly accepts attribute-bearing close forms such as
36
+ * `</untrusted-content foo>` — the matcher must cover them too.
31
37
  */
32
- function escapeCloseTagSentinels(s) {
33
- return s.replace(UNTRUSTED_CLOSE_TAG_VARIANT, ESCAPED_UNTRUSTED_CLOSE_TAG);
38
+ function escapeTagSentinels(s) {
39
+ return s
40
+ .replace(UNTRUSTED_OPEN_TAG_VARIANT, ESCAPED_UNTRUSTED_OPEN_TAG)
41
+ .replace(UNTRUSTED_CLOSE_TAG_VARIANT, ESCAPED_UNTRUSTED_CLOSE_TAG);
34
42
  }
35
- function unescapeCloseTagSentinels(s) {
36
- return s.replaceAll(ESCAPED_UNTRUSTED_CLOSE_TAG, '</untrusted-content>');
43
+ function unescapeTagSentinels(s) {
44
+ return s
45
+ .replaceAll(ESCAPED_UNTRUSTED_OPEN_TAG, '<untrusted-content>')
46
+ .replaceAll(ESCAPED_UNTRUSTED_CLOSE_TAG, '</untrusted-content>');
37
47
  }
38
48
  /**
39
49
  * Wrap a single untrusted string in an `<untrusted-content source="…">`
40
- * envelope, escaping any embedded close-tag variant so the envelope cannot be
41
- * broken out of.
50
+ * envelope, escaping any embedded open/close-tag variant so the envelope can
51
+ * neither be broken out of nor have a spoofed nested envelope planted inside.
42
52
  *
43
53
  * `undefined` and `null` are passed through untouched so callers can apply the wrapper
44
54
  * uniformly to optional fields without branching.
45
55
  *
46
56
  * Idempotent: when `text` is already a properly-shaped envelope for the SAME
47
57
  * `source` (starts with the matching OPEN tag, ends with CLOSE, and contains no
48
- * internal close-tag variants), the original string is returned unchanged so
58
+ * internal open/close-tag variants), the original string is returned unchanged so
49
59
  * `wrapUntrusted(wrapUntrusted(s, src), src) === wrapUntrusted(s, src)`.
50
60
  */
51
61
  export function wrapUntrusted(text, source) {
@@ -55,13 +65,14 @@ export function wrapUntrusted(text, source) {
55
65
  const close = '</untrusted-content>';
56
66
  if (text.startsWith(open) && text.endsWith(close) && text.length >= open.length + close.length) {
57
67
  const inner = text.slice(open.length, text.length - close.length);
58
- if (!UNTRUSTED_CLOSE_TAG_VARIANT.test(inner)) {
59
- UNTRUSTED_CLOSE_TAG_VARIANT.lastIndex = 0; // reset stateful /g regex
68
+ const hasVariant = UNTRUSTED_CLOSE_TAG_VARIANT.test(inner) || UNTRUSTED_OPEN_TAG_VARIANT.test(inner);
69
+ UNTRUSTED_CLOSE_TAG_VARIANT.lastIndex = 0; // reset stateful /g regexes
70
+ UNTRUSTED_OPEN_TAG_VARIANT.lastIndex = 0;
71
+ if (!hasVariant) {
60
72
  return text;
61
73
  }
62
- UNTRUSTED_CLOSE_TAG_VARIANT.lastIndex = 0;
63
74
  }
64
- return `${open}${escapeCloseTagSentinels(text)}${close}`;
75
+ return `${open}${escapeTagSentinels(text)}${close}`;
65
76
  }
66
77
  /**
67
78
  * Strip one `<untrusted-content>` envelope from `text` if present, returning raw
@@ -73,7 +84,7 @@ export function unwrapUntrusted(text) {
73
84
  const match = UNTRUSTED_ENVELOPE.exec(text);
74
85
  if (!match)
75
86
  return text;
76
- return unescapeCloseTagSentinels(match[1]);
87
+ return unescapeTagSentinels(match[1]);
77
88
  }
78
89
  /**
79
90
  * Recursively wrap every string key and value reachable inside `value` (strings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstone/mcp-server-microsoft-teams",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "mcpName": "io.github.mindstone/mcp-server-microsoft-teams",
5
5
  "description": "Microsoft 365 Teams via Graph: list chats, read/send messages, list teams/channels, presence.",
6
6
  "license": "FSL-1.1-MIT",
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.mindstone/mcp-server-microsoft-teams",
4
4
  "title": "Microsoft 365 Teams",
5
5
  "description": "Microsoft 365 Teams via Graph: list chats, read/send messages, list teams/channels, presence.",
6
- "version": "0.3.0",
6
+ "version": "0.3.1",
7
7
  "repository": {
8
8
  "url": "https://github.com/mindstone/mcp-servers",
9
9
  "source": "github",
@@ -14,7 +14,7 @@
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "@mindstone/mcp-server-microsoft-teams",
17
- "version": "0.3.0",
17
+ "version": "0.3.1",
18
18
  "runtimeHint": "npx",
19
19
  "transport": {
20
20
  "type": "stdio"