Skip to content

Commit e79e374

Browse files
authored
feat: forward _meta in upstream MCP requests for resources/read and prompts/get (#4141)
* feat: forward metadata in MCP requests for prompt and resource retrieval Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com> * feat: implement metadata validation and logging limits for MCP requests Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com> * feat: streamline resource and prompt retrieval with optional metadata injection Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com> * feat(tests): add security regression tests for meta_data validation in prompt and resource services Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com> * feat(tests): update tests to forward meta_data in resource service requests Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com> * fixup: ruff Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com> * feat: implement metadata validation limits to prevent excessive load on MCP servers Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com> * feat: enhance meta_data validation with depth and size checks across services and tests Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com> * feat: add configurable limits for user-supplied meta_data to prevent excessive load on MCP servers Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com> * fix: enhance logging security by ensuring only meta_data key names are logged, protecting sensitive information Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com> --------- Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com>
1 parent c04f65e commit e79e374

File tree

10 files changed

+612
-85
lines changed

10 files changed

+612
-85
lines changed

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,15 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
872872
# Default: 10000 characters
873873
# MAX_PARAM_LENGTH=10000
874874

875+
# CWE-400: Limits for user-supplied meta_data forwarded to upstream MCP servers.
876+
# Keeps arbitrarily large dicts from amplifying into downstream network/DB load.
877+
# Maximum number of top-level keys in meta_data (default: 16)
878+
# META_MAX_KEYS=16
879+
# Maximum nesting depth in meta_data (default: 2)
880+
# META_MAX_DEPTH=2
881+
# Maximum JSON-encoded byte size of meta_data (default: 4096)
882+
# META_MAX_BYTES=4096
883+
875884
# Regex patterns for dangerous input (JSON array)
876885
# Used to detect and block malicious input patterns
877886
# Default patterns:

.secrets.baseline

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "^.secrets.baseline|package-lock.json|Cargo.lock|scripts/sign_image.sh|scripts/zap|sonar-project.properties|uv.lock|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2026-04-13T09:59:07Z",
6+
"generated_at": "2026-04-14T12:29:01Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -108,71 +108,71 @@
108108
"hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3",
109109
"is_secret": false,
110110
"is_verified": false,
111-
"line_number": 977,
111+
"line_number": 986,
112112
"type": "Secret Keyword",
113113
"verified_result": null
114114
},
115115
{
116116
"hashed_secret": "7b4455a56fbf1d198e45e04c437488514645a82c",
117117
"is_secret": false,
118118
"is_verified": false,
119-
"line_number": 1003,
119+
"line_number": 1012,
120120
"type": "Secret Keyword",
121121
"verified_result": null
122122
},
123123
{
124124
"hashed_secret": "ac371b6dcce28a86c90d12bc57d946a800eebf17",
125125
"is_secret": false,
126126
"is_verified": false,
127-
"line_number": 1083,
127+
"line_number": 1092,
128128
"type": "Secret Keyword",
129129
"verified_result": null
130130
},
131131
{
132132
"hashed_secret": "0b6ec68df700dec4dcd64babd0eda1edccddace1",
133133
"is_secret": false,
134134
"is_verified": false,
135-
"line_number": 1088,
135+
"line_number": 1097,
136136
"type": "Secret Keyword",
137137
"verified_result": null
138138
},
139139
{
140140
"hashed_secret": "4ad6f0082ee224001beb3ca5c3e81c8ceea5ed86",
141141
"is_secret": false,
142142
"is_verified": false,
143-
"line_number": 1093,
143+
"line_number": 1102,
144144
"type": "Secret Keyword",
145145
"verified_result": null
146146
},
147147
{
148148
"hashed_secret": "cb32747fcfb55eaa194c8cd8e4ba7d49ada08a94",
149149
"is_secret": false,
150150
"is_verified": false,
151-
"line_number": 1099,
151+
"line_number": 1108,
152152
"type": "Secret Keyword",
153153
"verified_result": null
154154
},
155155
{
156156
"hashed_secret": "6c178d51b13520496dbc767ed3d9d7aa5803ac72",
157157
"is_secret": false,
158158
"is_verified": false,
159-
"line_number": 1111,
159+
"line_number": 1120,
160160
"type": "Secret Keyword",
161161
"verified_result": null
162162
},
163163
{
164164
"hashed_secret": "ca45060a53fd8a255d1a83ee8d2f025283ccc66e",
165165
"is_secret": false,
166166
"is_verified": false,
167-
"line_number": 1129,
167+
"line_number": 1138,
168168
"type": "Secret Keyword",
169169
"verified_result": null
170170
},
171171
{
172172
"hashed_secret": "910fbf00f58e9bcb095ea26a75cc1d9a3355e671",
173173
"is_secret": false,
174174
"is_verified": false,
175-
"line_number": 1190,
175+
"line_number": 1199,
176176
"type": "Secret Keyword",
177177
"verified_result": null
178178
}
@@ -5788,15 +5788,15 @@
57885788
"hashed_secret": "c377074d6473f35a91001981355da793dc808ffd",
57895789
"is_secret": false,
57905790
"is_verified": false,
5791-
"line_number": 700,
5791+
"line_number": 699,
57925792
"type": "Hex High Entropy String",
57935793
"verified_result": null
57945794
},
57955795
{
57965796
"hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684",
57975797
"is_secret": false,
57985798
"is_verified": false,
5799-
"line_number": 1102,
5799+
"line_number": 1101,
58005800
"type": "Basic Auth Credentials",
58015801
"verified_result": null
58025802
}
@@ -5814,7 +5814,7 @@
58145814
"hashed_secret": "ff37a98a9963d347e9749a5c1b3936a4a245a6ff",
58155815
"is_secret": false,
58165816
"is_verified": false,
5817-
"line_number": 2225,
5817+
"line_number": 2228,
58185818
"type": "Secret Keyword",
58195819
"verified_result": null
58205820
}
@@ -6128,15 +6128,15 @@
61286128
"hashed_secret": "a10b98d7340036e9c8c301704f623eddd733cc1a",
61296129
"is_secret": false,
61306130
"is_verified": false,
6131-
"line_number": 297,
6131+
"line_number": 346,
61326132
"type": "Hex High Entropy String",
61336133
"verified_result": null
61346134
},
61356135
{
61366136
"hashed_secret": "718cbcc5a4207c0d5f38e3a309bdba17cb0074b7",
61376137
"is_secret": false,
61386138
"is_verified": false,
6139-
"line_number": 3374,
6139+
"line_number": 3422,
61406140
"type": "Hex High Entropy String",
61416141
"verified_result": null
61426142
}
@@ -10660,15 +10660,15 @@
1066010660
"hashed_secret": "b4c9248600a42f8c38c01b632f392dbcb4c7b19a",
1066110661
"is_secret": false,
1066210662
"is_verified": false,
10663-
"line_number": 11085,
10663+
"line_number": 11086,
1066410664
"type": "Hex High Entropy String",
1066510665
"verified_result": null
1066610666
},
1066710667
{
1066810668
"hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa",
1066910669
"is_secret": false,
1067010670
"is_verified": false,
10671-
"line_number": 12222,
10671+
"line_number": 12223,
1067210672
"type": "Hex High Entropy String",
1067310673
"verified_result": null
1067410674
}

mcpgateway/common/validators.py

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,13 @@
5050
# Standard
5151
from html.parser import HTMLParser
5252
import ipaddress
53+
import json
5354
import logging
5455
from pathlib import Path
5556
import re
5657
import shlex
5758
import socket
58-
from typing import Any, Iterable, List, Optional, Pattern
59+
from typing import Any, Dict, Iterable, List, Optional, Pattern
5960
from urllib.parse import urlparse
6061
import uuid
6162

@@ -76,9 +77,7 @@
7677
_HTML_SPECIAL_CHARS_RE: Pattern[str] = re.compile(r'[<>"\']') # / removed per SEP-986
7778
_DANGEROUS_TEMPLATE_TAGS_RE: Pattern[str] = re.compile(r"<(script|iframe|object|embed|link|meta|base|form)\b", re.IGNORECASE)
7879
_EVENT_HANDLER_RE: Pattern[str] = re.compile(r"on\w+\s*=", re.IGNORECASE)
79-
_MIME_TYPE_RE: Pattern[str] = re.compile(
80-
r'^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+\.]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+\.]*(?:\s*;\s*[a-zA-Z0-9!#$&\-\^_+\.]+=(?:[a-zA-Z0-9!#$&\-\^_+\.]+|"[^"\r\n]*"))*$'
81-
)
80+
_MIME_TYPE_RE: Pattern[str] = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+\.]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+\.]*(?:\s*;\s*[a-zA-Z0-9!#$&\-\^_+\.]+=(?:[a-zA-Z0-9!#$&\-\^_+\.]+|"[^"\r\n]*"))*$')
8281
_URI_SCHEME_RE: Pattern[str] = re.compile(r"^[a-zA-Z][a-zA-Z0-9+\-.]*://")
8382
_SHELL_DANGEROUS_CHARS_RE: Pattern[str] = re.compile(r"[;&|`$(){}\[\]<>]")
8483
_ANSI_ESCAPE_RE: Pattern[str] = re.compile(r"\x1B\[[0-9;]*[A-Za-z]")
@@ -1815,3 +1814,60 @@ def validate_core_url(value: str, field_name: str = "URL") -> str:
18151814
The validated URL string.
18161815
"""
18171816
return SecurityValidator.validate_url(value, field_name)
1817+
1818+
1819+
# CWE-400: Limits for user-supplied meta_data forwarded to upstream MCP servers.
1820+
# Keeps arbitrarily large dicts from amplifying into downstream network/DB load.
1821+
# These are now read from config (settings.meta_max_keys, etc.) but kept as
1822+
# module-level aliases for backward-compatible imports.
1823+
META_MAX_KEYS: int = settings.meta_max_keys
1824+
META_MAX_DEPTH: int = settings.meta_max_depth
1825+
META_MAX_BYTES: int = settings.meta_max_bytes
1826+
1827+
1828+
def validate_meta_data(meta_data: Optional[Dict[str, Any]]) -> None:
1829+
"""Enforce size, key-count, and depth limits on user-supplied meta_data (CWE-400).
1830+
1831+
Args:
1832+
meta_data: The metadata dictionary to validate. ``None`` is always accepted.
1833+
1834+
Raises:
1835+
ValueError: if any limit is exceeded.
1836+
"""
1837+
max_keys = settings.meta_max_keys
1838+
max_depth = settings.meta_max_depth
1839+
max_bytes = settings.meta_max_bytes
1840+
1841+
if not meta_data:
1842+
return
1843+
if len(meta_data) > max_keys:
1844+
raise ValueError(f"meta_data exceeds maximum key count ({max_keys}): got {len(meta_data)}")
1845+
1846+
def _check_depth(obj: Any, depth: int) -> None:
1847+
"""Recursively enforce nesting depth, traversing both dicts and lists (CWE-400).
1848+
1849+
Lists are traversed without incrementing the depth counter so that a
1850+
list-of-dicts does not hide an extra level of dict nesting — e.g.
1851+
``{"k": [{"l2": {"l3": "x"}}]}`` is correctly caught as depth 3.
1852+
"""
1853+
if depth > max_depth:
1854+
raise ValueError(f"meta_data exceeds maximum nesting depth ({max_depth})")
1855+
if isinstance(obj, dict):
1856+
for v in obj.values():
1857+
_check_depth(v, depth + 1)
1858+
elif isinstance(obj, list):
1859+
for item in obj:
1860+
_check_depth(item, depth)
1861+
1862+
for v in meta_data.values():
1863+
_check_depth(v, 1)
1864+
1865+
try:
1866+
# CWE-20: Use strict json.dumps (no default=str) so non-serializable objects
1867+
# raise TypeError rather than being silently coerced — keeps the byte limit
1868+
# meaningful and matches the strict rejection behaviour used in prompt_service.
1869+
size = len(json.dumps(meta_data))
1870+
if size > max_bytes:
1871+
raise ValueError(f"meta_data exceeds maximum size ({max_bytes} bytes): got {size}")
1872+
except (TypeError, ValueError) as exc:
1873+
raise ValueError(f"meta_data is not serializable: {exc}") from exc

mcpgateway/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,9 @@ class Settings(BaseSettings):
360360
allowed_roots: List[str] = Field(default_factory=list, description="Allowed root paths for resource access")
361361
max_path_depth: int = Field(default=10, description="Maximum allowed path depth")
362362
max_param_length: int = Field(default=10000, description="Maximum parameter length")
363+
meta_max_keys: int = Field(default=16, description="Maximum number of keys in user-supplied meta_data forwarded to upstream MCP servers (CWE-400)")
364+
meta_max_depth: int = Field(default=2, description="Maximum nesting depth for user-supplied meta_data forwarded to upstream MCP servers (CWE-400)")
365+
meta_max_bytes: int = Field(default=4096, description="Maximum JSON-encoded byte size for user-supplied meta_data forwarded to upstream MCP servers (CWE-400)")
363366
dangerous_patterns: List[str] = Field(
364367
default_factory=lambda: [
365368
r"[;&|`$(){}\[\]<>]", # Shell metacharacters

0 commit comments

Comments
 (0)