Skip to content

Duo VR - Generated MR only adds a new line at end of file

This bug has been discovered during the Evaluation of Claude 3.7 (#525295 - closed), but it's not specific to Claude 3.7 and is probably in production for an unknown period of time.

Bug description

Duo Vulnerability Resolution generates an MR which contains only a new line at end of file.
It happens quite often, but it's unclear what are the exact conditions in which it happens.

How to reproduce

Trigger Vulnerability Resolution on the following vulnerability:

Here are the inputs provided to the AI Gateway, which can be used to reproduce the bug:

      "body": {
        "inputs": {
          "name": "Improper encoding or escaping of output",
          "filename": "base.py",
          "identifiers": "<report_identifiers>\n * CWE-116\n* Bandit Test ID B701\n* Bandit Test ID bandit.B701\n* A7:2017 - Cross-Site Scripting (XSS)\n* A03:2021 - Injection\n* python-jinja2-xss-autoescape-false-atomic\n* SAST Rules ID - python_escaping_rule-jinja2-autoescape-false\n</report_identifiers>",
          "vulnerable_code": "jinja_env = Environment(loader=BaseLoader())",
          "source_code": "from abc import ABC, abstractmethod\nfrom typing import Any, Optional, Sequence, Tuple, TypeVar\n\nfrom jinja2 import BaseLoader, Environment\nfrom langchain_core.prompts.chat import MessageLikeRepresentation\nfrom langchain_core.runnables import Runnable, RunnableBinding\n\nfrom ai_gateway.agents.typing import ModelMetadata\nfrom ai_gateway.auth.user import GitLabUser\nfrom ai_gateway.gitlab_features import GitLabUnitPrimitive, WrongUnitPrimitives\n\n__all__ = [\n    \"Agent\",\n    \"BaseAgentRegistry\",\n]\n\nInput = TypeVar(\"Input\")\nOutput = TypeVar(\"Output\")\n\njinja_env = Environment(loader=BaseLoader())\n\n\ndef _format_str(content: str, options: dict[str, Any]) -> str:\n    return jinja_env.from_string(content).render(options)\n\n\nclass Agent(RunnableBinding[Input, Output]):\n    name: str\n    unit_primitives: list[GitLabUnitPrimitive]\n\n    def __init__(\n        self, name: str, unit_primitives: list[GitLabUnitPrimitive], chain: Runnable\n    ):\n        super().__init__(name=name, unit_primitives=unit_primitives, bound=chain)  # type: ignore[call-arg]\n\n    # Assume that the prompt template keys map to roles. Subclasses can\n    # override this method to implement more complex logic.\n    @staticmethod\n    def _prompt_template_to_messages(\n        tpl: dict[str, str], options: dict[str, Any]\n    ) -> list[Tuple[str, str]]:\n        return list(tpl.items())\n\n    @classmethod\n    def build_messages(\n        cls, prompt_template: dict[str, str], options: dict[str, Any]\n    ) -> Sequence[MessageLikeRepresentation]:\n        messages = []\n\n        for role, template in cls._prompt_template_to_messages(\n            prompt_template, options\n        ):\n            messages.append((role, _format_str(template, options)))\n\n        return messages\n\n\nclass BaseAgentRegistry(ABC):\n    @abstractmethod\n    def get(\n        self,\n        agent_id: str,\n        options: Optional[dict[str, Any]] = None,\n        model_metadata: Optional[ModelMetadata] = None,\n    ) -> Agent:\n        pass\n\n    def get_on_behalf(\n        self, user: GitLabUser, agent_id: str, options: Optional[dict[str, Any]] = None\n    ) -> Agent:\n        agent = self.get(agent_id, options)\n\n        for unit_primitive in agent.unit_primitives:\n            if not user.can(unit_primitive):\n                raise WrongUnitPrimitives\n\n        return agent\n"
        },
        "prompt_version": "0.0.1-dev"
      },

Actual behavior

The generated MR contains only a new empty line at end of file.

image

Expected behavior

The generated MR must propose a meaningful diff fixing the vulnerability.

 from langchain_core.runnables import Runnable, RunnableBinding

 from ai_gateway.agents.typing import ModelMetadata
 from ai_gateway.auth.user import GitLabUser
 from ai_gateway.gitlab_features import GitLabUnitPrimitive, WrongUnitPrimitives

-jinja_env = Environment(loader=BaseLoader())
+jinja_env = Environment(loader=BaseLoader(), autoescape=True)

Preliminary analysis

Looking at the logs, we can see that the LLM response is correct:

<fixed_code>
<old_code>
from langchain_core.runnables import Runnable, RunnableBinding

from ai_gateway.agents.typing import ModelMetadata
from ai_gateway.auth.user import GitLabUser
from ai_gateway.gitlab_features import GitLabUnitPrimitive, WrongUnitPrimitives

jinja_env = Environment(loader=BaseLoader())
</old_code>
<new_code>
from langchain_core.runnables import Runnable, RunnableBinding

from ai_gateway.agents.typing import ModelMetadata
from ai_gateway.auth.user import GitLabUser
from ai_gateway.gitlab_features import GitLabUnitPrimitive, WrongUnitPrimitives

jinja_env = Environment(loader=BaseLoader(), autoescape=True)
</new_code>
</fixed_code>
Full response from LLM
## Analysis

The vulnerability report identifies a potential Cross-Site Scripting (XSS) vulnerability in the code, specifically related to Jinja2 templating. The issue is that the Jinja2 Environment is created without enabling the `autoescape` feature, which is indicated by the line:

```python
jinja_env = Environment(loader=BaseLoader())
```

This is concerning because:

1. CWE-116 refers to "Improper Encoding or Escaping of Output" which can lead to injection attacks
2. The report mentions XSS (Cross-Site Scripting) vulnerabilities
3. The code uses Jinja2 templating to render user-provided content without automatic escaping

In Jinja2, when `autoescape` is set to `False` (which is the default), the template engine doesn't automatically escape potentially dangerous characters in the output. This means that if user-controlled input is rendered through this template environment, it could lead to XSS vulnerabilities where malicious HTML or JavaScript could be injected and executed.

The `_format_str` function uses this environment to render templates, and this function is used in the `build_messages` method of the `Agent` class, potentially allowing user input to be rendered without proper escaping.

<fixed_code>
<old_code>
from langchain_core.runnables import Runnable, RunnableBinding

from ai_gateway.agents.typing import ModelMetadata
from ai_gateway.auth.user import GitLabUser
from ai_gateway.gitlab_features import GitLabUnitPrimitive, WrongUnitPrimitives

jinja_env = Environment(loader=BaseLoader())
</old_code>
<new_code>
from langchain_core.runnables import Runnable, RunnableBinding

from ai_gateway.agents.typing import ModelMetadata
from ai_gateway.auth.user import GitLabUser
from ai_gateway.gitlab_features import GitLabUnitPrimitive, WrongUnitPrimitives

jinja_env = Environment(loader=BaseLoader(), autoescape=True)
</new_code>
</fixed_code>

<is_false_positive>
false
</is_false_positive>

## Summary

1. The reported vulnerability is an XSS (Cross-Site Scripting) risk due to improper encoding or escaping of output in Jinja2 templates. The code creates a Jinja2 Environment without enabling the `autoescape` feature, which means that any HTML or JavaScript in the rendered content would not be automatically escaped.

2. The fix enables the `autoescape` feature in the Jinja2 Environment by adding the parameter `autoescape=True` to the Environment constructor. This ensures that any potentially dangerous characters in the output (like `<`, `>`, `&`, `"`, etc.) are automatically converted to their HTML entity equivalents, preventing the execution of injected scripts.

3. With this change, when the `_format_str` function renders templates using this environment, the output will be properly escaped, mitigating the risk of XSS attacks. This is particularly important since the code appears to be part of a system that processes user input and generates messages that might be displayed in a web context.

It means that the bug is somewhere in the MR generation logic.

Impact

This bug has a significant amount of occurrences.
On a dataset with 841 MRs generated by VR, I've found 87 instances of this "blank line" bug.

It affects about 10% of the MRs generated by Duo VR!

/cc @nmccorrison @mclausen35

Edited by 🤖 GitLab Bot 🤖