@zereight/mcp-gitlab 2.1.64 → 2.1.66

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.
@@ -17,3 +17,105 @@ export function normalizeGitLabApiUrl(url) {
17
17
  }
18
18
  return normalizedUrl;
19
19
  }
20
+ /**
21
+ * Dot segments anywhere in a decoded value, including after a percent-encoded
22
+ * separator ("..%2F..%2Fuser" decodes to a path-traversal sequence).
23
+ */
24
+ const DOT_SEGMENT_PATTERN = /(^|[\\/])\.\.?([\\/]|$)/;
25
+ /** A decoded value that starts with a path separator (absolute-path payload). */
26
+ const LEADING_SEPARATOR_PATTERN = /^[\\/]/;
27
+ /** NUL, newline and the other control characters: never valid in a path segment. */
28
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
29
+ /**
30
+ * How many times a value is percent-decoded before the checks run. A payload can be
31
+ * encoded more than once ("%252E%252E%252Fuser" only becomes "../user" after two
32
+ * decodes), so the checks run on the form the receiving server may eventually see.
33
+ */
34
+ const MAX_DECODE_PASSES = 5;
35
+ const INVALID_SEGMENT_MESSAGE = "Cannot use value as a GitLab URL path segment: it is or decodes to a '.'/'..' path segment, " +
36
+ "a control character such as NUL, or an absolute path";
37
+ const INVALID_PATH_MESSAGE = "Cannot use value as a GitLab URL path: it is empty or it has an empty, leading or trailing segment";
38
+ /**
39
+ * A percent escape that actually decodes to a byte: `%` followed by two hex digits.
40
+ *
41
+ * `decodeURIComponent` also throws for a `%` that starts no escape, and a literal
42
+ * percent sign is valid in a file or branch name ("report-100%.pdf"), so the two
43
+ * failures have to be told apart before either is treated as a broken payload.
44
+ */
45
+ const VALID_ESCAPE_PATTERN = /%[0-9a-fA-F]{2}/;
46
+ /**
47
+ * Percent-decode a value until decoding stops changing it.
48
+ *
49
+ * Returns null when the value cannot be reduced to a form that can be validated:
50
+ * a malformed escape next to a valid one, or more encoding layers than
51
+ * {@link MAX_DECODE_PASSES}. A guard must not treat "could not inspect the value"
52
+ * as "safe", so both cases are rejected by the callers rather than passed through.
53
+ *
54
+ * A `%` that starts no valid escape is a literal character instead: the value is
55
+ * returned as it is and the encoder turns the sign into `%25`, so
56
+ * `get_file_contents("report-100%.pdf")` keeps working. Only a value that mixes a
57
+ * real escape with a broken one cannot be inspected, and that form is rejected.
58
+ */
59
+ function decodeFully(value) {
60
+ let current = value;
61
+ for (let pass = 0; pass < MAX_DECODE_PASSES; pass++) {
62
+ let decoded;
63
+ try {
64
+ decoded = decodeURIComponent(current);
65
+ }
66
+ catch {
67
+ // Undecodable. With a valid escape left in it the value cannot be inspected
68
+ // reliably (the escape may still hide a separator or a dot segment), so it
69
+ // fails closed; without one every `%` is literal and `current` — already
70
+ // decoded as far as it can be — is the form the server will see.
71
+ return VALID_ESCAPE_PATTERN.test(current) ? null : current;
72
+ }
73
+ if (decoded === current) {
74
+ return current;
75
+ }
76
+ current = decoded;
77
+ }
78
+ return null;
79
+ }
80
+ /**
81
+ * Encode a single value for use as one GitLab URL path segment.
82
+ *
83
+ * Rejects values that are, decode to, or start with dot segments or an absolute
84
+ * path, so a caller-supplied id cannot escape the intended route prefix.
85
+ * Percent-encoded separators are decoded before the check because the receiving
86
+ * server may treat them as separators; a value may be encoded any number of times
87
+ * and is still validated in its fully decoded form.
88
+ *
89
+ * @throws {Error} when the value is a dot segment, contains one, contains a control
90
+ * character, starts with a separator, or is not valid percent-encoding
91
+ */
92
+ export function encodeGitLabPathSegment(value) {
93
+ const segment = String(value);
94
+ const decodedSegment = decodeFully(segment);
95
+ if (decodedSegment === null ||
96
+ DOT_SEGMENT_PATTERN.test(decodedSegment) ||
97
+ LEADING_SEPARATOR_PATTERN.test(decodedSegment) ||
98
+ CONTROL_CHARACTER_PATTERN.test(decodedSegment)) {
99
+ throw new Error(INVALID_SEGMENT_MESSAGE);
100
+ }
101
+ return encodeURIComponent(decodedSegment);
102
+ }
103
+ /**
104
+ * Encode a slash-separated path so each segment is encoded and validated
105
+ * individually. Legitimate encoded separators (for example a tag such as
106
+ * "release%2F1.0") stay allowed; dot segments are rejected.
107
+ *
108
+ * An empty path, or one with an empty, leading or trailing segment, is rejected
109
+ * rather than silently collapsed: `"/etc/passwd"` would otherwise encode to a
110
+ * leading-separator path instead of the intended relative one.
111
+ *
112
+ * @throws {Error} when the path is empty or has an empty segment, or when any
113
+ * segment is a dot segment or contains one
114
+ */
115
+ export function encodeGitLabPath(value) {
116
+ const segments = value.split("/");
117
+ if (segments.some(segment => segment === "")) {
118
+ throw new Error(INVALID_PATH_MESSAGE);
119
+ }
120
+ return segments.map(encodeGitLabPathSegment).join("/");
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.1.64",
3
+ "version": "2.1.66",
4
4
  "mcpName": "io.github.zereight/gitlab-mcp",
5
5
  "description": "GitLab MCP server for projects, merge requests, issues, pipelines, wiki, releases, and more",
6
6
  "keywords": [
@@ -78,6 +78,7 @@
78
78
  "express": "^5.1.0",
79
79
  "express-rate-limit": "^8.5.2",
80
80
  "fetch-cookie": "^3.1.0",
81
+ "jmespath": "^0.16.0",
81
82
  "open": "^10.2.0",
82
83
  "pino": "^9.7.0",
83
84
  "pino-pretty": "^13.0.0",
@@ -90,6 +91,7 @@
90
91
  },
91
92
  "devDependencies": {
92
93
  "@types/express": "^5.0.2",
94
+ "@types/jmespath": "^0.15.2",
93
95
  "@types/node": "^22.13.10",
94
96
  "@typescript-eslint/eslint-plugin": "^8.21.0",
95
97
  "@typescript-eslint/parser": "^8.21.0",