Conversation
5893758 to
cea8b6b
Compare
Lang-Akshay
left a comment
There was a problem hiding this comment.
Thanks for the PR @msureshkumar88 .
Please fix the following :
-
Failing unit tests
Run
make test -
Security Findings
| # | File | Line | Severity | CWE | Description |
|---|---|---|---|---|---|
| 1 | content_security.py:173 | 173 | High | CWE-390 | ContentPatternError defined but never raised by any service method. US-3 XSS/command-injection blocking is dead code with no backing implementation. |
| 2 | main.py:150 | 150 | High | CWE-755 | ContentPatternError not imported in main.py and has no global exception handler. Any future code raising it returns a generic 500 instead of HTTP 400. |
| 3 | prompt_service.py:905 | 905, 2443 | Medium | CWE-394 | except ContentPatternError as cpe: raise cpe blocks are unreachable dead code — validate_prompt_template() raises only TemplateValidationError, never ContentPatternError. |
| 4 | test_content_pattern_detection.py:61 | 61–63 | High | CWE-778 | Integration tests reference three config settings (content_pattern_detection_enabled, content_pattern_validation_mode, content_pattern_cache_enabled) that do not exist in config.py. Tests use raising=False so the monkeypatch silently no-ops; assertions against a 400 with violation_type: "xss_script_tag" will never pass against real code. |
| 5 | resource_service.py:65 | 65 | High | CWE-116 | No XSS/command-injection scanning applied to resource content. PR description claims US-3 covers both resources and prompts, but resource_service.py imports only ContentSizeError and ContentTypeError. A <script> payload stored in a resource is never detected. |
| 6 | main.py:2334 | 2334–2352 | Medium | CWE-209 | TemplateValidationError global handler returns exc.pattern (the matched regex) in the HTTP 400 response body. This leaks internal block-list policy to any authenticated caller, enabling targeted bypass crafting. |
| 7 | content_security.py:518 | 518–540 | Medium | CWE-209 | Bare except Exception as e wraps Jinja2 parse errors as TemplateValidationError(template_name, f"Invalid Jinja2 syntax: {str(e)}"). Jinja2 TemplateSyntaxError messages include the offending template fragment, which is then surfaced in the HTTP 400 reason field. |
| 8 | config.py:1637 | 1637 | Medium | CWE-400 | content_blocked_template_patterns is operator-configurable via env var and applied with re.search(..., re.IGNORECASE) with no timeout or complexity limit. A catastrophic backtracking pattern (ReDoS) in a misconfigured env causes service-level DoS on any prompt submission. |
| 9 | content_security.py:509 | 509 | Low | CWE-693 | Docstring claims meta.find_undeclared_variables(ast) "validates all filters and tests exist" and "raises TemplateAssertionError for nonexistent filters". This is factually wrong — the function returns a set of names and raises nothing. Incorrect documentation creates false security expectations. |
| 10 | .env.example:124 | 124 | Info | — | Comment says CONTENT_STRICT_MIME_VALIDATION=true but config.py defaults to False. Negligible for code but confusing for operators. |
Redundant Code
| # | File | Line(s) | Type | Description | Suggestion |
|---|---|---|---|---|---|
| 1 | prompt_service.py:905 | 905–916 | Dead code | except ContentPatternError as cpe: raise cpe after validate_prompt_template() — validate_prompt_template() never raises ContentPatternError | Remove block entirely, or implement US-3 service method so it can be raised |
| 2 | prompt_service.py:2443 | 2443–2455 | Dead code | Same unreachable catch block in update_prompt() | Same as above |
| 3 | content_security.py:173 | 173–227 | Dead code | ContentPatternError class defined and documented but never instantiated or raised by any service method | Implement US-3 or remove for this PR |
| 4 | test_content_pattern_detection.py | all | Unreachable tests | Tests reference three non-existent config keys with raising=False monkeypatches; assertions are never valid against actual runtime behavior |
Fix config key names to match config.py, or remove and track as future PR |
Lang-Akshay
left a comment
There was a problem hiding this comment.
Please implement above mentioned changes
- Implement US-3 malicious pattern detection (CWE-390, CWE-755, CWE-116) - Add missing configuration keys (CWE-778) - Make ContentPatternError handlers reachable (CWE-394) - Fix information disclosure vulnerabilities (CWE-209) - Add ReDoS protection with timeout (CWE-400) - Correct documentation about Jinja2 validation (CWE-693) - Add 21 comprehensive unit tests - Update existing tests to match security fixes All tests passing: 21 new + 277 existing tests with zero regressions. Closes #4072 Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
- Add content pattern detection service with configurable rules - Implement resource content validation in resource service - Add integration and unit tests for pattern detection - Fix HTTP 500 error in resource endpoint validation Closes #4072 Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
f9eb61c to
7970d31
Compare
All Issues Addressed ✅Hi @Lang-Akshay, I've completed all the requested and ready to review fixes for this PR. Here's a comprehensive summary: 🔒 Security Findings (10 Issues Fixed)Commit: Fixed Security Issues:
Changes Made:
Files Modified:
🎯 Feature Implementation (US-3 & US-4)Commit: Implemented Features:
🧹 Linting FixesCommit: 1.
|
|
Thanks for the updates @msureshkumar88 . Please make the following changes focusing on High and Medium Security hardeningPattern detection and template validation are the core of this PR. 1 High, 4 Medium, 5 Low, 3 Info findings. Two High findings completely undermine the security value of US-3.
Remediation highlights
Redundant Code
|
Lang-Akshay
left a comment
There was a problem hiding this comment.
Please implement above mentioned changes.
- Implement US-3 malicious pattern detection (CWE-390, CWE-755, CWE-116) - Add missing configuration keys (CWE-778) - Make ContentPatternError handlers reachable (CWE-394) - Fix information disclosure vulnerabilities (CWE-209) - Add ReDoS protection with timeout (CWE-400) - Correct documentation about Jinja2 validation (CWE-693) - Add 21 comprehensive unit tests - Update existing tests to match security fixes All tests passing: 21 new + 277 existing tests with zero regressions. Closes #4072 Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
- Add content pattern detection service with configurable rules - Implement resource content validation in resource service - Add integration and unit tests for pattern detection - Fix HTTP 500 error in resource endpoint validation Closes #4072 Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
7970d31 to
524a436
Compare
|
@Lang-Akshay Thank you for the thorough second review! I've addressed the HIGH and MEDIUM priority security findings (Issues 1-6) from your April 13th feedback. Here's what was completed: High Priority Fixes ✅1. CWE-400: ReDoS timeout Python version compatibility (commit
2. CWE-116: Input normalization bypass (commit
Medium Priority Fixes ✅3. CWE-20: Overly broad Jinja2 template regex (commit
4. CWE-20: False positives from broad patterns (commit
5. CWE-117: Log injection via unsanitized input (commit
6. CWE-20: Tool service bypasses pattern scanning (commit
7. Test assertions mismatch (commit
Additional Improvements ✅
Future Enhancements (LOW/INFO Priority) 💡The following LOW and INFO priority items have been identified as potential future improvements but are not blocking for this PR: 8. (Low - CWE-117): Template names sanitization in logs - Additional hardening opportunity These can be addressed in follow-up PRs as incremental improvements to the security posture. SummaryAll HIGH and MEDIUM priority security vulnerabilities have been resolved. The implementation is production-ready with comprehensive test coverage and zero regressions. Ready for final review and merge. |
- Implement US-3 malicious pattern detection (CWE-390, CWE-755, CWE-116) - Add missing configuration keys (CWE-778) - Make ContentPatternError handlers reachable (CWE-394) - Fix information disclosure vulnerabilities (CWE-209) - Add ReDoS protection with timeout (CWE-400) - Correct documentation about Jinja2 validation (CWE-693) - Add 21 comprehensive unit tests - Update existing tests to match security fixes All tests passing: 21 new + 277 existing tests with zero regressions. Closes #4072 Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
- Add content pattern detection service with configurable rules - Implement resource content validation in resource service - Add integration and unit tests for pattern detection - Fix HTTP 500 error in resource endpoint validation Closes #4072 Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
7bfaa7c to
7ded3cb
Compare
…prompt templates) Squashed rebase of PR #4072 (feat/content-security-us-3-us-4) onto origin/main. Closes #538. Implements: - US-3: Malicious pattern detection (XSS, template/command/SQL injection) - US-4: Prompt template validation (syntax + dangerous-pattern blocking) Co-authored-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com> Signed-off-by: Jonathan Springer <jps@s390x.com>
e49eaae to
9cae5b4
Compare
…prompt templates) Squashed rebase of PR #4072 (feat/content-security-us-3-us-4) onto origin/main. Closes #538. Implements: - US-3: Malicious pattern detection (XSS, template/command/SQL injection) - US-4: Prompt template validation (syntax + dangerous-pattern blocking) Co-authored-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com> Signed-off-by: Jonathan Springer <jps@s390x.com>
Ran the project's standard auto-fixers on the 19 Python files modified by this PR (per the pr-review skill workflow): uv tool run autoflake --remove-all-unused-imports --remove-unused-variables --in-place ... uv tool run 'isort<6' --profile black --line-length 200 ... uv tool run 'black>=24.0.0' --line-length 200 ... No semantic changes; only import ordering and formatting to line-length 200. Signed-off-by: Jonathan Springer <jps@s390x.com>
The integration tests in test_content_pattern_detection.py asserted fine-grained subtypes like "xss_script_tag", "template_injection_jinja", "command_injection_shell", etc., while _classify_violation() in content_security.py returns only the bare categories "xss", "template_injection", "command_injection", "sql_injection" (which is what the unit tests and global exception handler message in main.py already use). Standardize on the bare taxonomy: - Integration tests updated to assert bare categories - Removed assertions for 'pattern', 'validation_mode', and 'pattern_matched' keys that are intentionally omitted from the HTTP response per the CWE-209 information-disclosure fix at main.py:2467 - Added short security-rationale comments so the absence of these assertions is not mistaken for incomplete coverage by future contributors. Signed-off-by: Jonathan Springer <jps@s390x.com>
The two failing tests in test_main_error_handlers.py
(test_admin_add_prompt_template_validation_error and
test_admin_edit_prompt_template_validation_error) were asserting 400
but receiving 404 because the test_client fixture depends on
app_with_temp_db, which imports mcpgateway.main.app with
MCPGATEWAY_ADMIN_API_ENABLED force-disabled by the conftest bootstrap
(tests/conftest.py lines 74\u201378). As a result /admin/prompts and
/admin/prompts/{id}/edit were not present in the app's route table
and every admin-prefixed POST returned 404 before the mocked
register_prompt / update_prompt side_effect ever fired.
Wire the existing session-scoped main_app_with_admin_api fixture in as
a second dep of test_client. It mounts admin_router onto mcpgateway.main.app
exactly once per session and is already the repo's canonical way to
make admin routes addressable in unit tests (tests/unit/mcpgateway/test_ui_version.py,
tests/unit/mcpgateway/test_well_known.py, tests/e2e/test_admin_apis.py).
The fixture is side-effect-only; the docstring documents this so a
future contributor doesn't remove the seemingly unused parameter.
Signed-off-by: Jonathan Springer <jps@s390x.com>
…rvice Three related correctness fixes in resource_service.register_resource and resource_service.update_resource: 1. Missing db.rollback() on ContentSizeError/ContentTypeError PermissionError/IntegrityError handlers correctly call db.rollback() before re-raising, but the ContentSizeError and ContentTypeError branches (added alongside US-1/US-2) forgot to do so. On a validation failure the session was left in a dirty state; any subsequent commit in the same session could persist partial/invalid data or trigger transaction errors. 2. ContentPatternError being wrapped as ResourceError ContentPatternError wasn't caught explicitly, so it fell through to 'except Exception as e: raise ResourceError(f"Failed to update ...")'. That wrapping changed the exception type, which meant the FastAPI @app.exception_handler(ContentPatternError) in main.py never fired for resource create/update — callers got a 500 from the generic ResourceError instead of the structured 400 the global handler emits. Added explicit 'except ContentPatternError' handlers that rollback, log the violation via structured_logger, and re-raise unchanged so the global handler can format the response. 3. resource_update.title silently discarded update_resource used to copy resource_update.title into resource.title alongside uri/name/description. The hunk was dropped in this PR's rebase history; restored so title updates via API actually persist. Signed-off-by: Jonathan Springer <jps@s390x.com>
…rst hit detect_malicious_patterns() in lenient validation mode returned after the first pattern match, so co-occurring violations were silently dropped from the audit log. A payload like '<script>...</script> SELECT * FROM users && rm -rf /' only produced one 'Lenient mode: allowing ...' log line even though three independent patterns (XSS, SQL injection, command injection) fired. This undermines the whole point of lenient mode, which is to emit a complete audit trail while letting the request through. Changed the loop branch from 'return' to 'continue' so every pattern that matches is logged before the function returns normally. strict/moderate paths are unchanged (still raise on first match - fail-closed by design). Added a regression test (tests/unit/mcpgateway/services/test_content_pattern_detection.py) using caplog to assert that all three patterns log in the multi-vector case. The test uses 'Lenient mode: allowing' as the log prefix anchor, matching the logger.info call in content_security.py. Signed-off-by: Jonathan Springer <jps@s390x.com>
…se helper for templates Addresses the ReDoS soft-timeout finding (CWE-400): the existing threading.Thread(daemon=True) + thread.join(timeout) path on Python <3.13 is a soft timeout only. The worker thread cannot be killed, so a pathological regex pins a CPU core indefinitely even though the caller returns. Under load this accumulates zombie daemon threads. Changes: 1. Primary defense - input size cap. New settings.content_pattern_max_scan_size (default 200 KB) bounds worst-case scan time deterministically and is independent of regex engine behavior. detect_malicious_patterns() rejects oversize content with ContentPatternError(violation_type="content_too_large_to_scan") before entering the scan loop, and the global exception handler already translates that to HTTP 400. 2. Secondary defense - per-pattern timeout. Moved the Python version check out of the hot path and into a module-level _HAS_NATIVE_REGEX_TIMEOUT constant. Extracted settings.content_pattern_regex_timeout (default 1.0s) so ops can tune without code changes. Kept the threading fallback for Python 3.11/3.12 but renamed + commented so future contributors don't mistake it for a hard kill. 3. Patterns compiled once in __init__ (service is a singleton via get_content_security_service) instead of re-compiling on every request. _compile_patterns() tolerates malformed entries by logging and skipping them instead of killing the whole validator. 4. validate_prompt_template() now uses the same bounded scan path (compiled patterns + timeout) for content_blocked_template_patterns, which previously called re.search(pattern, template, re.IGNORECASE) with no timeout and no size guard - exactly the same ReDoS exposure as before but for prompt templates. Signed-off-by: Jonathan Springer <jps@s390x.com>
_process_single_tool_for_bulk() went straight from arg parsing to conflict
lookup and DB write without ever calling detect_malicious_patterns(). The
single-tool path register_tool() scans three fields (tool.name,
tool.description, JSON-serialized tool.input_schema) but bulk imports went
around all three - so an attacker with bulk-import access could inject
payloads that would have been rejected via POST /api/tools/{one}.
Copied the same three scans to the head of the try: block in
_process_single_tool_for_bulk(). The narrow (TypeError, ValueError) pass
around json.dumps matches register_tool()'s handling for non-serializable
input_schema values (e.g. MagicMock in tests) and is documented in a
comment so it's not mistaken for the generic 'except: pass' silent-failure
anti-pattern AGENTS.md calls out.
Signed-off-by: Jonathan Springer <jps@s390x.com>
register_resources_bulk() validated resource size and MIME type per-item
but never called detect_malicious_patterns() - the same three-line
content scan register_resource() does. Bulk callers could inject content
that would be rejected on POST /api/resources/{one}.
Copied the scan into the per-item loop in register_resources_bulk(),
keeping the same bytes-vs-str decoding + content_type='Resource content'
label as the single-resource path so audit logs and ContentPatternError
responses look identical regardless of entry point.
Signed-off-by: Jonathan Springer <jps@s390x.com>
…2 SSTI)
Switches prompt_service._JINJA_ENV from jinja2.Environment to
jinja2.sandbox.SandboxedEnvironment so rendering enforces the restriction
the regex blocklist in content_security.validate_prompt_template() only
tries to imply.
The regex blocklist scans template *source* for literal `__class__`,
`__import__`, `eval(`, etc. Every published Jinja2 SSTI bypass defeats
it trivially:
* hex escapes {{ ''|attr('\\x5f\\x5fclass\\x5f\\x5f') }}
* string concat {% set d = '_'*2 %}{{ ''|attr(d+'class'+d) }}
* attr() filter chains {{ request|attr('__class__')|attr('__mro__')[1] }}
* query-parameter injection {{ request|attr(request.args.attr_name) }}
Because the PR already renders user-supplied templates through
jinja2.Environment() (_compile_jinja_template -> .render), all of those
reached Python internals at runtime even when the template string passed
validation. See Jinja2 upstream's own recommendation to use
SandboxedEnvironment for this exact threat model; the same codebase
already uses SandboxedEnvironment in plugins/framework/loader/config.py.
Also hardens the render fallback path. _render_template used to do:
except Exception:
return template.format(**arguments)
on any Jinja2 failure. SecurityError is a subclass of Exception, so the
sandbox block would have been followed by a str.format() call - and
str.format's attribute-access syntax ({x.__class__}) reopens the same
hole SandboxedEnvironment just closed. Added a specific
'except JinjaSecurityError' branch that raises PromptError without
attempting the str.format fallback. Non-security Jinja2 errors (e.g.
TemplateSyntaxError on malformed templates) keep the existing fallback
for backward compat.
Signed-off-by: Jonathan Springer <jps@s390x.com>
…changes
The PR's original inline CHANGELOG entry was a stale snapshot duplicating
US-1/US-2 content already present in main; it was dropped during the
rebase conflict resolution. Adding a fresh [Unreleased] section that
covers the net changes landing with this PR:
Added:
- US-3 malicious pattern detection (all three entities, both single
and bulk paths)
- US-4 prompt template validation
- ReDoS-bounded pattern scanning (size cap + per-pattern timeout,
pre-compiled patterns)
Behavior Changes (require operator attention on upgrade):
- Prompts now render in jinja2.sandbox.SandboxedEnvironment -
templates relying on attribute access into Python internals will
raise PromptError at render. Regex blocklist is now a pre-flight
hint; the sandbox is the enforcement boundary.
- Content > CONTENT_PATTERN_MAX_SCAN_SIZE (default 200 KB) returns
400 with violation_type=content_too_large_to_scan regardless of
CONTENT_MAX_RESOURCE_SIZE.
- CONTENT_PATTERN_DETECTION_ENABLED and CONTENT_VALIDATE_PROMPT_TEMPLATES
both default to true on this release, unlike CONTENT_STRICT_MIME_VALIDATION.
Existing deployments with pre-existing matching content will start
returning 400s on next update.
Each behavior-change entry includes Impact / Why / Migration / Rollback
sub-sections so release comms and operators know what they're getting
and how to back out if needed.
Signed-off-by: Jonathan Springer <jps@s390x.com>
The earlier ReDoS and SandboxedEnvironment changes broke 20 unit tests
across two files. Three distinct root causes, fixed in one pass:
1. MagicMock settings tripping new comparisons/threading calls
(TestValidatePromptTemplate, TestTemplateValidationIntegration,
TestMaliciousPatternDetection lenient-mode tests,
TestTimeoutAndEdgeCases::test_lenient_mode_return_path)
detect_malicious_patterns() now reads
settings.content_pattern_max_scan_size for its size-cap guard and
settings.content_pattern_regex_timeout for the regex timeout, and
validate_prompt_template() inherits the same timeout for its template
pattern scan. Tests that patch settings with a bare MagicMock ended
up with 'int > MagicMock' (size cap) and 'max(MagicMock, 0)'
(thread.join inside _regex_search_with_timeout) TypeErrors.
Fixed two ways:
- TestValidatePromptTemplate / TestTemplateValidationIntegration:
disabled Step-0 pattern detection (they test template validation
only) and stubbed content_pattern_regex_timeout +
content_pattern_max_scan_size so the template-pattern scan can
run cleanly with its mock-supplied blocklist.
- TestMaliciousPatternDetection / TestTimeoutAndEdgeCases:
set content_pattern_max_scan_size and content_pattern_regex_timeout
to real numbers before instantiating the service.
2. _regex_search_with_timeout signature change
(TestRegexSearchWithTimeout::test_regex_search_with_timeout_timeout)
The helper now receives a compiled re.Pattern from the scan hot path,
but one existing test still passes a raw string. Widened the helper
to accept either - it coerces str -> re.compile(IGNORECASE|DOTALL)
internally (matching detect_malicious_patterns semantics) so existing
callers and test fixtures keep working.
3. Tests coupled to old implementation details
(TestTimeoutAndEdgeCases::test_timeout_error_handling,
test_python313_timeout_path_coverage)
These probed 'is re.search called' and 'is sys.version_info (3,13)
taken', both of which are no longer observable after the refactor
(re.search isn't on the hot path; the version check collapsed into
the module-level _HAS_NATIVE_REGEX_TIMEOUT constant).
Rewrote both to patch _HAS_NATIVE_REGEX_TIMEOUT directly and either
stub _regex_search_with_timeout (fallback path) or stub
_compiled_blocked_patterns with a MagicMock whose .search() accepts
the timeout kwarg (native path - real re.Pattern.search on Py<3.13
rejects it). Test renamed to test_python313_native_timeout_path_coverage
to reflect what it actually verifies.
All 116 tests in both files pass, plus the broader unit suite across
touched modules (1999 passed in 11.75s).
Signed-off-by: Jonathan Springer <jps@s390x.com>
9cae5b4 to
3983762
Compare
… convention Fixes pylint W0621 (redefined-outer-name) and W0404 (reimported) at tool_service.py:1889, 2358, and 6045. These three pattern-scan sites inside register_tool(), _process_single_tool_for_bulk(), and update_tool() were each doing `# Standard\nimport json\nschema_str = json.dumps(tool.input_schema)` while the module already has `import json` at line 23 for json.JSONDecodeError (httpx raises stdlib exceptions - see note at line 23). Two things got straightened out: 1. Local json imports removed. Module-level `json` is still imported for `except json.JSONDecodeError` at lines 4914/4939/4950 where httpx error handling needs the stdlib exception type. 2. `json.dumps(tool.input_schema)` swapped for the house-standard `orjson.dumps(tool.input_schema).decode()` pattern used at lines 561, 1603, 1656, 4923, 4943, 5664, 5666, 5703, 6790, 6792. orjson is already imported at module line 43. Behavior is preserved for the MagicMock test-compat path: orjson raises orjson.JSONEncodeError, which is a subclass of TypeError, so the existing `except (TypeError, ValueError)` catch still works identically. Verified: - pylint --disable=all --enable=W0621,W0404 tool_service.py -> 10.00/10 - tests/unit/mcpgateway/services/test_tool_service.py: all pass Signed-off-by: Jonathan Springer <jps@s390x.com>
3983762 to
fb2b084
Compare
* feat(security): content security US-3 (malicious patterns) and US-4 (prompt templates) Squashed rebase of PR #4072 (feat/content-security-us-3-us-4) onto origin/main. Closes #538. Implements: - US-3: Malicious pattern detection (XSS, template/command/SQL injection) - US-4: Prompt template validation (syntax + dangerous-pattern blocking) Co-authored-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com> Signed-off-by: Jonathan Springer <jps@s390x.com> * chore(lint): apply autoflake/isort/black to touched files Ran the project's standard auto-fixers on the 19 Python files modified by this PR (per the pr-review skill workflow): uv tool run autoflake --remove-all-unused-imports --remove-unused-variables --in-place ... uv tool run 'isort<6' --profile black --line-length 200 ... uv tool run 'black>=24.0.0' --line-length 200 ... No semantic changes; only import ordering and formatting to line-length 200. Signed-off-by: Jonathan Springer <jps@s390x.com> * fix(tests): align violation_type assertions to bare category taxonomy The integration tests in test_content_pattern_detection.py asserted fine-grained subtypes like "xss_script_tag", "template_injection_jinja", "command_injection_shell", etc., while _classify_violation() in content_security.py returns only the bare categories "xss", "template_injection", "command_injection", "sql_injection" (which is what the unit tests and global exception handler message in main.py already use). Standardize on the bare taxonomy: - Integration tests updated to assert bare categories - Removed assertions for 'pattern', 'validation_mode', and 'pattern_matched' keys that are intentionally omitted from the HTTP response per the CWE-209 information-disclosure fix at main.py:2467 - Added short security-rationale comments so the absence of these assertions is not mistaken for incomplete coverage by future contributors. Signed-off-by: Jonathan Springer <jps@s390x.com> * fix(tests): mount admin router for /admin/prompts HTTP tests The two failing tests in test_main_error_handlers.py (test_admin_add_prompt_template_validation_error and test_admin_edit_prompt_template_validation_error) were asserting 400 but receiving 404 because the test_client fixture depends on app_with_temp_db, which imports mcpgateway.main.app with MCPGATEWAY_ADMIN_API_ENABLED force-disabled by the conftest bootstrap (tests/conftest.py lines 74\u201378). As a result /admin/prompts and /admin/prompts/{id}/edit were not present in the app's route table and every admin-prefixed POST returned 404 before the mocked register_prompt / update_prompt side_effect ever fired. Wire the existing session-scoped main_app_with_admin_api fixture in as a second dep of test_client. It mounts admin_router onto mcpgateway.main.app exactly once per session and is already the repo's canonical way to make admin routes addressable in unit tests (tests/unit/mcpgateway/test_ui_version.py, tests/unit/mcpgateway/test_well_known.py, tests/e2e/test_admin_apis.py). The fixture is side-effect-only; the docstring documents this so a future contributor doesn't remove the seemingly unused parameter. Signed-off-by: Jonathan Springer <jps@s390x.com> * fix(resources): rollback + propagate validation errors in resource_service Three related correctness fixes in resource_service.register_resource and resource_service.update_resource: 1. Missing db.rollback() on ContentSizeError/ContentTypeError PermissionError/IntegrityError handlers correctly call db.rollback() before re-raising, but the ContentSizeError and ContentTypeError branches (added alongside US-1/US-2) forgot to do so. On a validation failure the session was left in a dirty state; any subsequent commit in the same session could persist partial/invalid data or trigger transaction errors. 2. ContentPatternError being wrapped as ResourceError ContentPatternError wasn't caught explicitly, so it fell through to 'except Exception as e: raise ResourceError(f"Failed to update ...")'. That wrapping changed the exception type, which meant the FastAPI @app.exception_handler(ContentPatternError) in main.py never fired for resource create/update — callers got a 500 from the generic ResourceError instead of the structured 400 the global handler emits. Added explicit 'except ContentPatternError' handlers that rollback, log the violation via structured_logger, and re-raise unchanged so the global handler can format the response. 3. resource_update.title silently discarded update_resource used to copy resource_update.title into resource.title alongside uri/name/description. The hunk was dropped in this PR's rebase history; restored so title updates via API actually persist. Signed-off-by: Jonathan Springer <jps@s390x.com> * fix(security): lenient mode must scan every pattern, not return on first hit detect_malicious_patterns() in lenient validation mode returned after the first pattern match, so co-occurring violations were silently dropped from the audit log. A payload like '<script>...</script> SELECT * FROM users && rm -rf /' only produced one 'Lenient mode: allowing ...' log line even though three independent patterns (XSS, SQL injection, command injection) fired. This undermines the whole point of lenient mode, which is to emit a complete audit trail while letting the request through. Changed the loop branch from 'return' to 'continue' so every pattern that matches is logged before the function returns normally. strict/moderate paths are unchanged (still raise on first match - fail-closed by design). Added a regression test (tests/unit/mcpgateway/services/test_content_pattern_detection.py) using caplog to assert that all three patterns log in the multi-vector case. The test uses 'Lenient mode: allowing' as the log prefix anchor, matching the logger.info call in content_security.py. Signed-off-by: Jonathan Springer <jps@s390x.com> * fix(security): cap pattern scan input size; pre-compile patterns; reuse helper for templates Addresses the ReDoS soft-timeout finding (CWE-400): the existing threading.Thread(daemon=True) + thread.join(timeout) path on Python <3.13 is a soft timeout only. The worker thread cannot be killed, so a pathological regex pins a CPU core indefinitely even though the caller returns. Under load this accumulates zombie daemon threads. Changes: 1. Primary defense - input size cap. New settings.content_pattern_max_scan_size (default 200 KB) bounds worst-case scan time deterministically and is independent of regex engine behavior. detect_malicious_patterns() rejects oversize content with ContentPatternError(violation_type="content_too_large_to_scan") before entering the scan loop, and the global exception handler already translates that to HTTP 400. 2. Secondary defense - per-pattern timeout. Moved the Python version check out of the hot path and into a module-level _HAS_NATIVE_REGEX_TIMEOUT constant. Extracted settings.content_pattern_regex_timeout (default 1.0s) so ops can tune without code changes. Kept the threading fallback for Python 3.11/3.12 but renamed + commented so future contributors don't mistake it for a hard kill. 3. Patterns compiled once in __init__ (service is a singleton via get_content_security_service) instead of re-compiling on every request. _compile_patterns() tolerates malformed entries by logging and skipping them instead of killing the whole validator. 4. validate_prompt_template() now uses the same bounded scan path (compiled patterns + timeout) for content_blocked_template_patterns, which previously called re.search(pattern, template, re.IGNORECASE) with no timeout and no size guard - exactly the same ReDoS exposure as before but for prompt templates. Signed-off-by: Jonathan Springer <jps@s390x.com> * fix(security): close bulk tool registration US-3 bypass _process_single_tool_for_bulk() went straight from arg parsing to conflict lookup and DB write without ever calling detect_malicious_patterns(). The single-tool path register_tool() scans three fields (tool.name, tool.description, JSON-serialized tool.input_schema) but bulk imports went around all three - so an attacker with bulk-import access could inject payloads that would have been rejected via POST /api/tools/{one}. Copied the same three scans to the head of the try: block in _process_single_tool_for_bulk(). The narrow (TypeError, ValueError) pass around json.dumps matches register_tool()'s handling for non-serializable input_schema values (e.g. MagicMock in tests) and is documented in a comment so it's not mistaken for the generic 'except: pass' silent-failure anti-pattern AGENTS.md calls out. Signed-off-by: Jonathan Springer <jps@s390x.com> * fix(security): close bulk resource registration US-3 bypass register_resources_bulk() validated resource size and MIME type per-item but never called detect_malicious_patterns() - the same three-line content scan register_resource() does. Bulk callers could inject content that would be rejected on POST /api/resources/{one}. Copied the scan into the per-item loop in register_resources_bulk(), keeping the same bytes-vs-str decoding + content_type='Resource content' label as the single-resource path so audit logs and ContentPatternError responses look identical regardless of entry point. Signed-off-by: Jonathan Springer <jps@s390x.com> * fix(security): render prompt templates in SandboxedEnvironment (Jinja2 SSTI) Switches prompt_service._JINJA_ENV from jinja2.Environment to jinja2.sandbox.SandboxedEnvironment so rendering enforces the restriction the regex blocklist in content_security.validate_prompt_template() only tries to imply. The regex blocklist scans template *source* for literal `__class__`, `__import__`, `eval(`, etc. Every published Jinja2 SSTI bypass defeats it trivially: * hex escapes {{ ''|attr('\\x5f\\x5fclass\\x5f\\x5f') }} * string concat {% set d = '_'*2 %}{{ ''|attr(d+'class'+d) }} * attr() filter chains {{ request|attr('__class__')|attr('__mro__')[1] }} * query-parameter injection {{ request|attr(request.args.attr_name) }} Because the PR already renders user-supplied templates through jinja2.Environment() (_compile_jinja_template -> .render), all of those reached Python internals at runtime even when the template string passed validation. See Jinja2 upstream's own recommendation to use SandboxedEnvironment for this exact threat model; the same codebase already uses SandboxedEnvironment in plugins/framework/loader/config.py. Also hardens the render fallback path. _render_template used to do: except Exception: return template.format(**arguments) on any Jinja2 failure. SecurityError is a subclass of Exception, so the sandbox block would have been followed by a str.format() call - and str.format's attribute-access syntax ({x.__class__}) reopens the same hole SandboxedEnvironment just closed. Added a specific 'except JinjaSecurityError' branch that raises PromptError without attempting the str.format fallback. Non-security Jinja2 errors (e.g. TemplateSyntaxError on malformed templates) keep the existing fallback for backward compat. Signed-off-by: Jonathan Springer <jps@s390x.com> * docs(changelog): add Unreleased section for US-3, US-4, and behavior changes The PR's original inline CHANGELOG entry was a stale snapshot duplicating US-1/US-2 content already present in main; it was dropped during the rebase conflict resolution. Adding a fresh [Unreleased] section that covers the net changes landing with this PR: Added: - US-3 malicious pattern detection (all three entities, both single and bulk paths) - US-4 prompt template validation - ReDoS-bounded pattern scanning (size cap + per-pattern timeout, pre-compiled patterns) Behavior Changes (require operator attention on upgrade): - Prompts now render in jinja2.sandbox.SandboxedEnvironment - templates relying on attribute access into Python internals will raise PromptError at render. Regex blocklist is now a pre-flight hint; the sandbox is the enforcement boundary. - Content > CONTENT_PATTERN_MAX_SCAN_SIZE (default 200 KB) returns 400 with violation_type=content_too_large_to_scan regardless of CONTENT_MAX_RESOURCE_SIZE. - CONTENT_PATTERN_DETECTION_ENABLED and CONTENT_VALIDATE_PROMPT_TEMPLATES both default to true on this release, unlike CONTENT_STRICT_MIME_VALIDATION. Existing deployments with pre-existing matching content will start returning 400s on next update. Each behavior-change entry includes Impact / Why / Migration / Rollback sub-sections so release comms and operators know what they're getting and how to back out if needed. Signed-off-by: Jonathan Springer <jps@s390x.com> * test(security): repair unit tests broken by ReDoS/sandbox refactor The earlier ReDoS and SandboxedEnvironment changes broke 20 unit tests across two files. Three distinct root causes, fixed in one pass: 1. MagicMock settings tripping new comparisons/threading calls (TestValidatePromptTemplate, TestTemplateValidationIntegration, TestMaliciousPatternDetection lenient-mode tests, TestTimeoutAndEdgeCases::test_lenient_mode_return_path) detect_malicious_patterns() now reads settings.content_pattern_max_scan_size for its size-cap guard and settings.content_pattern_regex_timeout for the regex timeout, and validate_prompt_template() inherits the same timeout for its template pattern scan. Tests that patch settings with a bare MagicMock ended up with 'int > MagicMock' (size cap) and 'max(MagicMock, 0)' (thread.join inside _regex_search_with_timeout) TypeErrors. Fixed two ways: - TestValidatePromptTemplate / TestTemplateValidationIntegration: disabled Step-0 pattern detection (they test template validation only) and stubbed content_pattern_regex_timeout + content_pattern_max_scan_size so the template-pattern scan can run cleanly with its mock-supplied blocklist. - TestMaliciousPatternDetection / TestTimeoutAndEdgeCases: set content_pattern_max_scan_size and content_pattern_regex_timeout to real numbers before instantiating the service. 2. _regex_search_with_timeout signature change (TestRegexSearchWithTimeout::test_regex_search_with_timeout_timeout) The helper now receives a compiled re.Pattern from the scan hot path, but one existing test still passes a raw string. Widened the helper to accept either - it coerces str -> re.compile(IGNORECASE|DOTALL) internally (matching detect_malicious_patterns semantics) so existing callers and test fixtures keep working. 3. Tests coupled to old implementation details (TestTimeoutAndEdgeCases::test_timeout_error_handling, test_python313_timeout_path_coverage) These probed 'is re.search called' and 'is sys.version_info (3,13) taken', both of which are no longer observable after the refactor (re.search isn't on the hot path; the version check collapsed into the module-level _HAS_NATIVE_REGEX_TIMEOUT constant). Rewrote both to patch _HAS_NATIVE_REGEX_TIMEOUT directly and either stub _regex_search_with_timeout (fallback path) or stub _compiled_blocked_patterns with a MagicMock whose .search() accepts the timeout kwarg (native path - real re.Pattern.search on Py<3.13 rejects it). Test renamed to test_python313_native_timeout_path_coverage to reflect what it actually verifies. All 116 tests in both files pass, plus the broader unit suite across touched modules (1999 passed in 11.75s). Signed-off-by: Jonathan Springer <jps@s390x.com> * refactor(tool_service): drop local json imports; use orjson per house convention Fixes pylint W0621 (redefined-outer-name) and W0404 (reimported) at tool_service.py:1889, 2358, and 6045. These three pattern-scan sites inside register_tool(), _process_single_tool_for_bulk(), and update_tool() were each doing `# Standard\nimport json\nschema_str = json.dumps(tool.input_schema)` while the module already has `import json` at line 23 for json.JSONDecodeError (httpx raises stdlib exceptions - see note at line 23). Two things got straightened out: 1. Local json imports removed. Module-level `json` is still imported for `except json.JSONDecodeError` at lines 4914/4939/4950 where httpx error handling needs the stdlib exception type. 2. `json.dumps(tool.input_schema)` swapped for the house-standard `orjson.dumps(tool.input_schema).decode()` pattern used at lines 561, 1603, 1656, 4923, 4943, 5664, 5666, 5703, 6790, 6792. orjson is already imported at module line 43. Behavior is preserved for the MagicMock test-compat path: orjson raises orjson.JSONEncodeError, which is a subclass of TypeError, so the existing `except (TypeError, ValueError)` catch still works identically. Verified: - pylint --disable=all --enable=W0621,W0404 tool_service.py -> 10.00/10 - tests/unit/mcpgateway/services/test_tool_service.py: all pass Signed-off-by: Jonathan Springer <jps@s390x.com> --------- Signed-off-by: Jonathan Springer <jps@s390x.com> Co-authored-by: Jonathan Springer <jps@s390x.com> Co-authored-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com> Signed-off-by: Brian Hussey <brian.hussey@ie.ibm.com>
🔗 Related Issue
Closes #538
📝 Summary
This PR completes the content security validation implementation for issue #538 by adding US-3 (Block Malicious Patterns) and US-4 (Validate Prompt Templates) to the existing US-1 and US-2 implementation.
Status:
RateLimiterPlugin)What This PR Adds:
ContentPatternErrorexception class for pattern violations🏷️ Type of Change
🧪 Verification
make lintmake testmake coverageTest Results:
✅ Checklist
make black isort pre-commit)📓 What's New in This PR
🆕 US-3: Malicious Pattern Detection (Block Malicious Patterns)
New Functionality:
<script>,javascript:, event handlers);,&&,||, backticks)New Exception:
ContentPatternError- Raised when malicious pattern detectedConfiguration Options:
Files Modified:
mcpgateway/services/content_security.py(lines 173-220)ContentPatternErrorexception classmcpgateway/services/resource_service.pyContentPatternErrormcpgateway/services/prompt_service.py(lines 51, 905-918, 2427-2440, 670-676, 2149-2157)ContentPatternErrorimportregister_prompt()andupdate_prompt()🆕 US-4: Prompt Template Validation
New Functionality:
New Exception:
TemplateValidationError- Raised for template syntax/security issuesConfiguration Options:
Validation Steps:
Files Modified:
mcpgateway/services/content_security.py(lines 509-580)validate_prompt_template()methodmcpgateway/services/prompt_service.pyregister_prompt()andupdate_prompt()🔄 Rebase Conflict Resolution
After rebasing
feat/block-malicious-patternsontoorigin/mainwithgit rebase -X theirs, 14 tests failed due to merge conflicts. This PR fixes all issues:Issues Fixed:
ContentPatternErrorclass definition (restored lines 173-220)content_securityvariable inupdate_resource()(fixed line 2983)bulk_mime_typevariable in bulk registration (fixed 3 occurrences)ContentPatternError(added to prompt service)Tests Fixed:
📚 Complete Configuration Reference
US-3 & US-4 Configuration (This PR)
US-1 & US-2 Configuration (Already Merged)
🔒 Security Improvements (This PR)
New Security Features:
__import__,eval,exec, file operationsCombined with Existing (US-1 & US-2):
📋 US-5: Rate Limiting (Future Work)
Analysis: US-5 can be achieved using the existing
RateLimiterPluginwith minimal configuration.Configuration Example:
{ "name": "ContentCreationRateLimiter", "kind": "plugins.rate_limiter.rate_limiter.RateLimiterPlugin", "hooks": ["tool_pre_invoke"], "config": { "by_user": "3/m", # 3 requests per minute per user "by_tenant": "100/m", # 100 requests per minute per tenant "algorithm": "sliding_window", "backend": "redis" } }Capabilities:
🎯 Summary
This PR Completes:
Already in Main:
Future Work:
Branch:
feat/block-malicious-patterns(implements US-3 & US-4)Recommended Rename:
feat/content-security-us-3-us-4for clarity