diff --git a/src/dashboard.py b/src/dashboard.py index f537d650..a900056f 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -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)) diff --git a/src/modules/Peer.py b/src/modules/Peer.py index 23cf32b3..68421f42 100644 --- a/src/modules/Peer.py +++ b/src/modules/Peer.py @@ -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": diff --git a/src/modules/Utilities.py b/src/modules/Utilities.py index 661d3500..191f3b2f 100644 --- a/src/modules/Utilities.py +++ b/src/modules/Utilities.py @@ -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: """ diff --git a/src/modules/WireguardConfiguration.py b/src/modules/WireguardConfiguration.py index f3e80ecc..a77b9d34 100644 --- a/src/modules/WireguardConfiguration.py +++ b/src/modules/WireguardConfiguration.py @@ -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: