mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2026-10-30 01:12:07 +00:00
Merge commit from fork
User-controlled template sources were rendered with the unrestricted
jinja2.Template, allowing authenticated users to achieve remote code
execution. All rendering now goes through a single shared
RenderSandboxedTemplate() helper backed by jinja2.sandbox, which denies
unsafe attribute access (e.g. __globals__, __class__) while keeping the
intended templating features working:
- POST /api/email/preview: Body and Subject (GHSA-8q4m-52pw-hfmw)
- POST /api/email/send: Body and Subject (same sink class)
- POST /api/updateWireguardConfigurationInfo (OverridePeerSettings):
values were rendered before validation, so the payload executed
before __validateOverridePeerSettings could reject it
- Peer.downloadPeer(): rendered configuration file includes raw
OverridePeerSettings values; also rendered for dashboard clients
via GetAssignedPeers
Verified: lipsum.__globals__, __class__.__mro__, TemplateReference,
cycler/joiner/namespace escape chains all raise SecurityError; normal
templates ({{ configuration.* }}, {{ peer.* }}, filters, loops) render
unchanged.
Co-authored-by: DaanSelen <80752476+DaanSelen@users.noreply.github.com>
This commit is contained in:
+7
-9
@@ -9,8 +9,7 @@ from zipfile import ZipFile
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import sqlalchemy
|
||||
from jinja2 import Template
|
||||
from flask import Flask, request, render_template, session, send_file, current_app, redirect, url_for
|
||||
from flask import Flask, request, render_template, session, send_file, current_app
|
||||
from flask_cors import CORS
|
||||
from icmplib import ping, traceroute
|
||||
from flask.json.provider import DefaultJSONProvider
|
||||
@@ -20,7 +19,8 @@ from sqlalchemy import RowMapping
|
||||
|
||||
from modules.Utilities import (
|
||||
RegexMatch, StringToBoolean, ValidateDNSAddress,
|
||||
GenerateWireguardPublicKey, GenerateWireguardPrivateKey
|
||||
GenerateWireguardPublicKey, GenerateWireguardPrivateKey,
|
||||
RenderSandboxedTemplate
|
||||
)
|
||||
from packaging import version
|
||||
from modules.Email import EmailSender
|
||||
@@ -1597,10 +1597,9 @@ def API_Email_Send():
|
||||
if configuration is not None:
|
||||
fp, p = configuration.searchPeer(data.get('Peer'))
|
||||
if fp:
|
||||
template = Template(body)
|
||||
download = p.downloadPeer()
|
||||
body = template.render(peer=p.toJson(), configurationFile=download)
|
||||
subject = Template(data.get('Subject', '')).render(peer=p.toJson(), configurationFile=download)
|
||||
body = RenderSandboxedTemplate(body, peer=p.toJson(), configurationFile=download)
|
||||
subject = RenderSandboxedTemplate(data.get('Subject', ''), peer=p.toJson(), configurationFile=download)
|
||||
if data.get('IncludeAttachment', False):
|
||||
u = str(uuid4())
|
||||
peerName = p.toJson().get('name', '').strip()
|
||||
@@ -1629,11 +1628,10 @@ def API_Email_PreviewBody():
|
||||
return ResponseObject(False, "Peer does not exist")
|
||||
|
||||
try:
|
||||
template = Template(body)
|
||||
download = p.downloadPeer()
|
||||
return ResponseObject(data={
|
||||
"Body": Template(body).render(peer=p.toJson(), configurationFile=download),
|
||||
"Subject": Template(subject).render(peer=p.toJson(), configurationFile=download)
|
||||
"Body": RenderSandboxedTemplate(body, peer=p.toJson(), configurationFile=download),
|
||||
"Subject": RenderSandboxedTemplate(subject, peer=p.toJson(), configurationFile=download)
|
||||
})
|
||||
except Exception as e:
|
||||
return ResponseObject(False, message=str(e))
|
||||
|
||||
+3
-3
@@ -7,12 +7,12 @@ import json
|
||||
import os, subprocess, uuid, random, re
|
||||
from datetime import timedelta
|
||||
|
||||
import jinja2
|
||||
import sqlalchemy as db
|
||||
from .PeerJob import PeerJob
|
||||
from flask import current_app
|
||||
from .PeerShareLink import PeerShareLink
|
||||
from .Utilities import GenerateWireguardPublicKey, CheckAddress, ValidateDNSAddress
|
||||
from .Utilities import GenerateWireguardPublicKey, CheckAddress, ValidateDNSAddress, \
|
||||
RenderSandboxedTemplate
|
||||
|
||||
|
||||
class Peer:
|
||||
@@ -237,7 +237,7 @@ class Peer:
|
||||
if val is not None and ((type(val) is str and len(val) > 0) or (type(val) is int and val > 0)):
|
||||
final["file"] += f"{key} = {val}\n"
|
||||
|
||||
final["file"] = jinja2.Template(final["file"]).render(configuration=self.configuration)
|
||||
final["file"] = RenderSandboxedTemplate(final["file"], configuration=self.configuration)
|
||||
|
||||
|
||||
if self.configuration.Protocol == "awg":
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import re, ipaddress
|
||||
import subprocess
|
||||
import sqlalchemy
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
sandboxEnvironment = SandboxedEnvironment()
|
||||
|
||||
def RenderSandboxedTemplate(source: str, **context) -> str:
|
||||
"""
|
||||
Render a Jinja2 template within a sandboxed environment. Template sources built with
|
||||
user-provided content (e.g. email body, peer configuration file) must go through this
|
||||
function, so unsafe attribute access (such as __globals__) is denied and template
|
||||
injection cannot escalate into arbitrary code execution.
|
||||
@param source: Template source string, may contain user-provided content
|
||||
@param context: Variables passed into the template
|
||||
@return: Rendered template
|
||||
"""
|
||||
return sandboxEnvironment.from_string(source).render(**context)
|
||||
|
||||
def RegexMatch(regex, text) -> bool:
|
||||
"""
|
||||
|
||||
@@ -3,7 +3,6 @@ WireGuard Configuration
|
||||
"""
|
||||
from typing import Any
|
||||
|
||||
import jinja2
|
||||
import sqlalchemy, random, shutil, configparser, ipaddress, os, subprocess, time, re, uuid, psutil, traceback
|
||||
from zipfile import ZipFile
|
||||
from datetime import datetime, timedelta
|
||||
@@ -21,7 +20,8 @@ from .Utilities import StringToBoolean, \
|
||||
ValidateDNSAddress, \
|
||||
ValidateEndpointAllowedIPs, \
|
||||
CheckAddress, \
|
||||
CheckPeerKey
|
||||
CheckPeerKey, \
|
||||
RenderSandboxedTemplate
|
||||
from .WireguardConfigurationInfo import WireguardConfigurationInfo, PeerGroupsClass
|
||||
from .DashboardWebHooks import DashboardWebHooks
|
||||
|
||||
@@ -1242,7 +1242,7 @@ class WireguardConfiguration:
|
||||
elif key == "OverridePeerSettings":
|
||||
for (key, val) in value.items():
|
||||
try:
|
||||
status, msg = self.__validateOverridePeerSettings(key, jinja2.Template(val).render(configuration=self.toJson()))
|
||||
status, msg = self.__validateOverridePeerSettings(key, RenderSandboxedTemplate(val, configuration=self.toJson()))
|
||||
if not status:
|
||||
return False, msg, key
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user