brightspace-mcp-server 5.1.26 → 5.1.27

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.
@@ -16,8 +16,9 @@ import { inflateRawSync } from "node:zlib";
16
16
  * parsing a header that does not exist.
17
17
  *
18
18
  * Nothing here throws. A truncated download, an unsupported compression
19
- * method, or a ZIP64 archive all return null, because the caller's fallback
20
- * is simply to report the file without its text.
19
+ * method, a ZIP64 archive, or an entry that inflates past the size budget all
20
+ * return null, because the caller's fallback is simply to report the file
21
+ * without its text.
21
22
  */
22
23
  const EOCD_SIGNATURE = 0x06054b50;
23
24
  const CENTRAL_FILE_SIGNATURE = 0x02014b50;
@@ -26,6 +27,13 @@ const EOCD_MAX_COMMENT = 0xffff;
26
27
  const ZIP64_MARKER = 0xffffffff;
27
28
  const METHOD_STORED = 0;
28
29
  const METHOD_DEFLATE = 8;
30
+ /**
31
+ * Most bytes one document may inflate to, across every entry read from it.
32
+ * DEFLATE reaches ~1000:1, so a 50 MB download could otherwise inflate to
33
+ * tens of gigabytes and take the server down (issue #380). Real Office XML
34
+ * sits far below this.
35
+ */
36
+ export const MAX_INFLATED_BYTES = 64 * 1024 * 1024;
29
37
  /** Find the End of Central Directory record by scanning backwards. */
30
38
  function findEocd(buffer) {
31
39
  const earliest = Math.max(0, buffer.length - EOCD_MIN_SIZE - EOCD_MAX_COMMENT);
@@ -70,7 +78,13 @@ function readCentralDirectory(buffer) {
70
78
  }
71
79
  return entries;
72
80
  }
73
- function readEntryData(buffer, entry) {
81
+ /**
82
+ * An entry's bytes, or null when it would exceed `limit`. The declared size
83
+ * is checked first, but a header can lie, so inflation is capped as well.
84
+ */
85
+ function readEntryData(buffer, entry, limit) {
86
+ if (entry.uncompressedSize > limit)
87
+ return null;
74
88
  const header = entry.localHeaderOffset;
75
89
  if (header + 30 > buffer.length)
76
90
  return null;
@@ -84,18 +98,19 @@ function readEntryData(buffer, entry) {
84
98
  return null;
85
99
  const raw = buffer.subarray(start, end);
86
100
  if (entry.method === METHOD_STORED)
87
- return Buffer.from(raw);
101
+ return raw.length > limit ? null : Buffer.from(raw);
88
102
  if (entry.method !== METHOD_DEFLATE)
89
103
  return null;
90
104
  try {
91
- return inflateRawSync(raw);
105
+ // Throws a RangeError past maxOutputLength.
106
+ return inflateRawSync(raw, { maxOutputLength: Math.max(1, limit) });
92
107
  }
93
108
  catch {
94
109
  return null;
95
110
  }
96
111
  }
97
112
  /** One entry from a ZIP archive by exact name, or null. Never throws. */
98
- export function extractZipEntry(buffer, entryName) {
113
+ export function extractZipEntry(buffer, entryName, limit = MAX_INFLATED_BYTES) {
99
114
  try {
100
115
  const entries = readCentralDirectory(buffer);
101
116
  if (!entries)
@@ -103,7 +118,7 @@ export function extractZipEntry(buffer, entryName) {
103
118
  const entry = entries.find((e) => e.name === entryName);
104
119
  if (!entry)
105
120
  return null;
106
- return readEntryData(buffer, entry);
121
+ return readEntryData(buffer, entry, limit);
107
122
  }
108
123
  catch {
109
124
  return null;
@@ -145,7 +160,22 @@ export function officeDocumentText(buffer) {
145
160
  const names = listZipEntries(buffer);
146
161
  if (!names)
147
162
  return null;
148
- const read = (name) => xmlToText(extractZipEntry(buffer, name)?.toString("utf-8") ?? "");
163
+ // One budget for the whole document, so a PPTX can't spread a zip bomb
164
+ // across many slides that each fit on their own. An entry that fails to
165
+ // read (oversize included) ends the walk: retrying the rest would re-inflate
166
+ // up to the budget once per entry.
167
+ let budget = MAX_INFLATED_BYTES;
168
+ const read = (name) => {
169
+ if (budget <= 0)
170
+ return "";
171
+ const data = extractZipEntry(buffer, name, budget);
172
+ if (!data) {
173
+ budget = 0;
174
+ return "";
175
+ }
176
+ budget -= data.length;
177
+ return xmlToText(data.toString("utf-8"));
178
+ };
149
179
  let text = "";
150
180
  if (names.includes("word/document.xml")) {
151
181
  text = read("word/document.xml");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "5.1.26",
3
+ "version": "5.1.27",
4
4
  "mcpName": "io.github.RohanMuppa/brightspace",
5
5
  "description": "MCP server for Brightspace (D2L). Check grades, due dates, assignments, announcements, syllabus, rosters and more via Claude, ChatGPT, Cursor, Windsurf, or any MCP client.",
6
6
  "type": "module",