Repository: nonebot/nonebot2

Issue:
Bug: MessageTemplate 存在CQ码注入问题
**描述问题：**

使用 Message.template 的 format 时可能存在CQ码注入问题

**如何复现？**

```python
>>> from nonebot.adapters.onebot.v11 import Message
>>> Message.template(Message("{nickname}是我老婆!")).format(nickname="[CQ:at,qq=all]")
[MessageSegment(type='at', data={'qq': 'all'}), MessageSegment(type='text', data={'text': '是我老婆!'})]
```

**期望的结果**

上面的例子中，`"[CQ:at,qq=all]"` 应被解析为文本，而不是 `at` 类型

**环境信息：**

 - OS: Linux Ubuntu 20.04
 - Python Version: 3.8.10
 - Nonebot Version: 2.0.0b1

**截图或日志**

![Snipaste_2022-02-09_16-03-35.png](https://s2.loli.net/2022/02/09/IO76rTnLsAfuHjZ.png)

Bug: MessageTemplate 存在CQ码注入问题
**描述问题：**

使用 Message.template 的 format 时可能存在CQ码注入问题

**如何复现？**

```python
>>> from nonebot.adapters.onebot.v11 import Message
>>> Message.template(Message("{nickname}是我老婆!")).format(nickname="[CQ:at,qq=all]")
[MessageSegment(type='at', data={'qq': 'all'}), MessageSegment(type='text', data={'text': '是我老婆!'})]
```

**期望的结果**

上面的例子中，`"[CQ:at,qq=all]"` 应被解析为文本，而不是 `at` 类型

**环境信息：**

 - OS: Linux Ubuntu 20.04
 - Python Version: 3.8.10
 - Nonebot Version: 2.0.0b1

**截图或日志**

![Snipaste_2022-02-09_16-03-35.png](https://s2.loli.net/2022/02/09/IO76rTnLsAfuHjZ.png)



Discussion:

Resolving patch:
diff --git a/nonebot/internal/adapter/template.py b/nonebot/internal/adapter/template.py
--- a/nonebot/internal/adapter/template.py
+++ b/nonebot/internal/adapter/template.py
@@ -104,7 +104,7 @@ def _vformat(
         if recursion_depth < 0:
             raise ValueError("Max string recursion exceeded")
 
-        results: List[Any] = []
+        results: List[Any] = [self.factory()]
 
         for (literal_text, field_name, format_spec, conversion) in self.parse(
             format_string
@@ -162,10 +162,7 @@ def _vformat(
                 formatted_text = self.format_field(obj, str(format_control))
                 results.append(formatted_text)
 
-        return (
-            self.factory(functools.reduce(self._add, results or [""])),
-            auto_arg_index,
-        )
+        return functools.reduce(self._add, results), auto_arg_index
 
     def format_field(self, value: Any, format_spec: str) -> Any:
         formatter: Optional[FormatSpecFunc] = self.format_specs.get(format_spec)


Test patch:
diff --git a/tests/test_adapters/test_template.py b/tests/test_adapters/test_template.py
--- a/tests/test_adapters/test_template.py
+++ b/tests/test_adapters/test_template.py
@@ -1,4 +1,4 @@
-from utils import make_fake_message
+from utils import escape_text, make_fake_message
 
 
 def test_template_basis():
@@ -10,11 +10,8 @@ def test_template_basis():
 
 
 def test_template_message():
-    from nonebot.adapters import MessageTemplate
-
     Message = make_fake_message()
-
-    template = MessageTemplate("{a:custom}{b:text}{c:image}", Message)
+    template = Message.template("{a:custom}{b:text}{c:image}")
 
     @template.add_format_spec
     def custom(input: str) -> str:
@@ -33,3 +30,12 @@ def custom(input: str) -> str:
     assert template.format_map(format_args) == formatted
     assert formatted.extract_plain_text() == "custom-custom!text"
     assert str(formatted) == "custom-custom!text[fake:image]"
+
+
+def test_message_injection():
+    Message = make_fake_message()
+
+    template = Message.template("{name}Is Bad")
+    message = template.format(name="[fake:image]")
+
+    assert message.extract_plain_text() == escape_text("[fake:image]Is Bad")
diff --git a/tests/test_examples/test_weather.py b/tests/test_examples/test_weather.py
--- a/tests/test_examples/test_weather.py
+++ b/tests/test_examples/test_weather.py
@@ -29,7 +29,11 @@ async def test_weather(app: App):
         event = make_fake_event(_message=msg, _to_me=True)()
 
         ctx.receive_event(bot, event)
-        ctx.should_call_send(event, Message("你想查询的城市 南京 暂不支持，请重新输入！"), True)
+        ctx.should_call_send(
+            event,
+            Message.template("你想查询的城市 {} 暂不支持，请重新输入！").format("南京"),
+            True,
+        )
         ctx.should_rejected()
 
         msg = Message("北京")
@@ -53,7 +57,11 @@ async def test_weather(app: App):
         event = make_fake_event(_message=msg)()
 
         ctx.receive_event(bot, event)
-        ctx.should_call_send(event, Message("你想查询的城市 杭州 暂不支持，请重新输入！"), True)
+        ctx.should_call_send(
+            event,
+            Message.template("你想查询的城市 {} 暂不支持，请重新输入！").format("杭州"),
+            True,
+        )
         ctx.should_rejected()
 
         msg = Message("北京")
diff --git a/tests/utils.py b/tests/utils.py
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -6,7 +6,14 @@
     from nonebot.adapters import Event, Message
 
 
-def make_fake_message() -> Type["Message"]:
+def escape_text(s: str, *, escape_comma: bool = True) -> str:
+    s = s.replace("&", "&amp;").replace("[", "&#91;").replace("]", "&#93;")
+    if escape_comma:
+        s = s.replace(",", "&#44;")
+    return s
+
+
+def make_fake_message():
     from nonebot.adapters import Message, MessageSegment
 
     class FakeMessageSegment(MessageSegment):
@@ -42,6 +49,10 @@ def _construct(msg: Union[str, Iterable[Mapping]]):
                     yield FakeMessageSegment(**seg)
             return
 
+        def __add__(self, other):
+            other = escape_text(other) if isinstance(other, str) else other
+            return super().__add__(other)
+
     return FakeMessage