mcp-server-kubernetes 4.0.8 → 4.1.0

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.
@@ -1,6 +1,6 @@
1
1
  export declare const serverConfig: {
2
2
  readonly name: "kubernetes";
3
- readonly version: "4.0.8";
3
+ readonly version: "4.1.0";
4
4
  readonly capabilities: {
5
5
  readonly resources: {};
6
6
  readonly tools: {};
@@ -1,6 +1,6 @@
1
1
  export const serverConfig = {
2
2
  name: "kubernetes",
3
- version: "4.0.8",
3
+ version: "4.1.0",
4
4
  capabilities: {
5
5
  resources: {},
6
6
  tools: {},
@@ -35,10 +35,39 @@ const DANGEROUS_FLAGS = new Set([
35
35
  "as",
36
36
  "as-group",
37
37
  "as-uid",
38
+ // Writes to attacker-chosen filesystem paths
39
+ "profile-output",
40
+ "log-file",
41
+ "cache-dir",
38
42
  ]);
39
43
  const SHORT_ALIASES = new Set([
40
44
  "s", // -s is an alias for --server
41
45
  ]);
46
+ // Short flags that consume a value. pflag stops parsing a shorthand cluster at
47
+ // the first of these: everything after it inside the token is that flag's
48
+ // value ("-ojsonpath={.items[0]}"), not more flags. Every other letter is a
49
+ // boolean shorthand, which pflag parses and then keeps going past — so the
50
+ // scan in shortFlagLetters() must keep going too.
51
+ //
52
+ // Comparison is case-sensitive, exactly like pflag: "-A" (--all-namespaces)
53
+ // and "-a" are different flags.
54
+ //
55
+ // Only letters that take a value in *every* command that defines them belong
56
+ // here. "-f" is deliberately absent: it is --filename for apply/delete but the
57
+ // boolean --follow for logs, so pflag keeps parsing the cluster after it.
58
+ // Leaving a letter out is always the safe direction — at worst an attached
59
+ // value containing "s" is refused, and the split form ("-f /path") or the long
60
+ // form ("--filename=/path") still works.
61
+ const SHORT_VALUE_FLAGS = new Set([
62
+ "c", // --container
63
+ "k", // --kustomize
64
+ "l", // --selector
65
+ "L", // --label-columns
66
+ "n", // --namespace
67
+ "o", // --output
68
+ "s", // --server (dangerous; listed so the scan stops after it as well)
69
+ "v", // --v (log level)
70
+ ]);
42
71
  // helm exposes the same exfiltration surface as kubectl, but under "kube-"
43
72
  // prefixed flag names (e.g. --kube-apiserver instead of --server). We add
44
73
  // those here so the argv-level guard covers helm invocations too. Context
@@ -69,21 +98,48 @@ function normalizeFlagName(raw) {
69
98
  const eq = name.indexOf("=");
70
99
  if (eq !== -1)
71
100
  name = name.slice(0, eq);
72
- return name.toLowerCase();
101
+ // kubectl and helm install pflag's WordSepNormalizeFunc, which treats "_" as
102
+ // equivalent to "-" in long flag names: "--insecure_skip_tls_verify" and
103
+ // "--insecure-skip-tls-verify" are the same flag. Normalize the same way so
104
+ // both spellings compare equal against the sets above.
105
+ return name.toLowerCase().replace(/_/g, "-");
73
106
  }
74
- // Extract the short-flag letter from a single-dash token, or null if the token
75
- // is not a single-dash short flag. pflag lets a short flag carry its value
76
- // attached with no separator ("-shttp://x" == "--server http://x"), so the
77
- // letter that matters is always the first character after the dash; the rest of
78
- // the token is the attached value (or a boolean-flag cluster). Long "--" flags
79
- // never attach a value without "=", so normalizeFlagName already handles them.
80
- function shortFlagLetter(raw) {
107
+ // Return every letter pflag would parse as a flag out of a single-dash token,
108
+ // in order, or null if the token is not a single-dash short flag.
109
+ //
110
+ // pflag walks a shorthand cluster letter by letter: a boolean shorthand is
111
+ // consumed and parsing continues with the next letter, while a value-taking
112
+ // shorthand swallows the remainder of the token as its value. "-Aowide" is
113
+ // therefore "-A -o wide", not a single unknown flag, so every letter pflag
114
+ // reaches has to be checked — not just the first one after the dash.
115
+ //
116
+ // Long "--" flags never attach a value without "=", so normalizeFlagName
117
+ // already handles them.
118
+ function shortFlagLetters(raw) {
81
119
  if (!raw.startsWith("-") || raw.startsWith("--"))
82
120
  return null;
83
121
  const body = raw.slice(1);
84
122
  if (body.length === 0)
85
123
  return null;
86
- return body[0].toLowerCase();
124
+ const letters = [];
125
+ for (let i = 0; i < body.length; i++) {
126
+ const letter = body[i];
127
+ letters.push(letter);
128
+ // "-o=json": the "=" and everything after it is the value.
129
+ if (body[i + 1] === "=")
130
+ break;
131
+ // A value-taking shorthand consumes the rest of the token as its value,
132
+ // so no further letters are parsed as flags.
133
+ if (SHORT_VALUE_FLAGS.has(letter))
134
+ break;
135
+ }
136
+ return letters;
137
+ }
138
+ function hasDangerousShortFlag(raw) {
139
+ const letters = shortFlagLetters(raw);
140
+ if (letters === null)
141
+ return false;
142
+ return letters.some((letter) => SHORT_ALIASES.has(letter));
87
143
  }
88
144
  function isDangerousFlagName(rawName, fromArgs) {
89
145
  const name = normalizeFlagName(rawName);
@@ -91,13 +147,12 @@ function isDangerousFlagName(rawName, fromArgs) {
91
147
  return true;
92
148
  // Short aliases (-s) are only meaningful when they appear as a CLI token,
93
149
  // not as a key in the `flags` object. Match both the bare/split forms
94
- // (normalizeFlagName -> "s") and the attached form "-sURL" (whose first
95
- // post-dash character is the flag pflag actually parses).
150
+ // (normalizeFlagName -> "s") and the attached/clustered forms ("-sURL",
151
+ // "-Ashttps://attacker"), where pflag parses the alias out of the cluster.
96
152
  if (fromArgs) {
97
153
  if (SHORT_ALIASES.has(name))
98
154
  return true;
99
- const short = shortFlagLetter(rawName);
100
- if (short !== null && SHORT_ALIASES.has(short))
155
+ if (hasDangerousShortFlag(rawName))
101
156
  return true;
102
157
  }
103
158
  return false;
@@ -160,10 +215,10 @@ export function assertSafeArgv(args) {
160
215
  const name = normalizeFlagName(tok);
161
216
  if (ARGV_DANGEROUS_FLAGS.has(name) || SHORT_ALIASES.has(name))
162
217
  reject(tok);
163
- // Attached short-flag form ("-sURL"): match the first post-dash character,
164
- // which is the flag pflag parses regardless of the trailing value.
165
- const short = shortFlagLetter(tok);
166
- if (short !== null && SHORT_ALIASES.has(short))
218
+ // Attached/clustered short-flag forms ("-sURL", "-Ashttps://attacker"):
219
+ // match every letter pflag would parse out of the cluster, not just the
220
+ // first one.
221
+ if (hasDangerousShortFlag(tok))
167
222
  reject(tok);
168
223
  }
169
224
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-kubernetes",
3
- "version": "4.0.8",
3
+ "version": "4.1.0",
4
4
  "description": "MCP server for interacting with Kubernetes clusters via kubectl",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -46,7 +46,7 @@
46
46
  "@opentelemetry/sdk-node": "0.219.0",
47
47
  "@opentelemetry/semantic-conventions": "1.40.0",
48
48
  "express": "4.21.2",
49
- "js-yaml": "4.1.1",
49
+ "js-yaml": "4.3.0",
50
50
  "yaml": "2.8.3",
51
51
  "zod": "3.25.76"
52
52
  },