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.
- package/build/utils/zip-extract.js +38 -8
- package/package.json +1 -1
|
@@ -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,
|
|
20
|
-
* is simply to report the file
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|