From a2f191a32229256dd41deadf97786fe41ce04cbb Mon Sep 17 00:00:00 2001 From: Jamal Date: Wed, 5 Jul 2023 20:56:01 +0100 Subject: [PATCH] Replace JIRA Arbitrary Code Execution vulnerability with finer grain API wrapper (#6992) This fixes #4833 and the critical vulnerability https://nvd.nist.gov/vuln/detail/CVE-2023-34540 Previously, the JIRA API Wrapper had a mode that simply pipelined user input into an `exec()` function. [The intended use of the 'other' mode is to cover any of Atlassian's API that don't have an existing interface](https://github.com/hwchase17/langchain/blob/cc33bde74ff2e050a400e4451e04ff5b32c4a7bd/langchain/tools/jira/prompt.py#L24) Fortunately all of the [Atlassian JIRA API methods are subfunctions of their `Jira` class](https://atlassian-python-api.readthedocs.io/jira.html), so this implementation calls these subfunctions directly. As well as passing a string representation of the function to call, the implementation flexibly allows for optionally passing args and/or keyword-args. These are given as part of the dictionary input. Example: ``` { "function": "update_issue_field", #function to execute "args": [ #list of ordered args similar to other examples in this JiraAPIWrapper "key", {"summary": "New summary"} ], "kwargs": {} #dict of key value keyword-args pairs } ``` the above is equivalent to `self.jira.update_issue_field("key", {"summary": "New summary"})` Alternate query schema designs are welcome to make querying easier without passing and evaluating arbitrary python code. I considered parsing (without evaluating) input python code and extracting the function, args, and kwargs from there and then pipelining them into the callable function via `*f(args, **kwargs)` - but this seemed more direct. @vowelparrot @dev2049 --------- Co-authored-by: Jamal Rahman --- langchain/tools/jira/prompt.py | 9 ++++---- langchain/utilities/jira.py | 13 ++++++++---- .../utilities/test_jira_api.py | 21 +++++++++++++++++++ 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/langchain/tools/jira/prompt.py b/langchain/tools/jira/prompt.py index a50c373c9a..08f0602304 100644 --- a/langchain/tools/jira/prompt.py +++ b/langchain/tools/jira/prompt.py @@ -25,11 +25,12 @@ JIRA_CATCH_ALL_PROMPT = """ This tool is a wrapper around atlassian-python-api's Jira API. There are other dedicated tools for fetching all projects, and creating and searching for issues, use this tool if you need to perform any other actions allowed by the atlassian-python-api Jira API. - The input to this tool is line of python code that calls a function from atlassian-python-api's Jira API - For example, to update the summary field of an issue, you would pass in the following string: - self.jira.update_issue_field(key, {{"summary": "New summary"}}) + The input to this tool is a dictionary specifying a function from atlassian-python-api's Jira API, + as well as a list of arguments and dictionary of keyword arguments to pass into the function. + For example, to get all the users in a group, while increasing the max number of results to 100, you would + pass in the following dictionary: {{"function": "get_all_users_from_group", "args": ["group"], "kwargs": {{"limit":100}} }} or to find out how many projects are in the Jira instance, you would pass in the following string: - self.jira.projects() + {{"function": "projects"}} For more information on the Jira API, refer to https://atlassian-python-api.readthedocs.io/jira.html """ diff --git a/langchain/utilities/jira.py b/langchain/utilities/jira.py index 3f3719437e..f3fe4dfe3e 100644 --- a/langchain/utilities/jira.py +++ b/langchain/utilities/jira.py @@ -188,10 +188,15 @@ class JiraAPIWrapper(BaseModel): return self.confluence.create_page(**dict(params)) def other(self, query: str) -> str: - context = {"self": self} - exec(f"result = {query}", context) - result = context["result"] - return str(result) + try: + import json + except ImportError: + raise ImportError( + "json is not installed. Please install it with `pip install json`" + ) + params = json.loads(query) + jira_function = getattr(self.jira, params["function"]) + return jira_function(*params.get("args", []), **params.get("kwargs", {})) def run(self, mode: str, query: str) -> str: if mode == "jql": diff --git a/tests/integration_tests/utilities/test_jira_api.py b/tests/integration_tests/utilities/test_jira_api.py index fa44a6014d..3a74e9e943 100644 --- a/tests/integration_tests/utilities/test_jira_api.py +++ b/tests/integration_tests/utilities/test_jira_api.py @@ -41,3 +41,24 @@ def test_create_confluence_page() -> None: output = jira.run("create_page", create_page_dict) assert "type" in output assert "page" in output + + +def test_other() -> None: + """Non-exhaustive test for accessing other JIRA API methods""" + jira = JiraAPIWrapper() + issue_create_dict = """ + { + "function":"issue_create", + "kwargs": { + "fields": { + "summary": "Test Summary", + "description": "Test Description", + "issuetype": {"name": "Bug"}, + "project": {"key": "TP"} + } + } + } + """ + output = jira.run("other", issue_create_dict) + assert "id" in output + assert "key" in output