From 1e974b185d2caff288d73356220e1096910f9b96 Mon Sep 17 00:00:00 2001 From: Elena ``of Valhalla'' Grandi Date: Sat, 27 Jan 2024 11:26:24 +0100 Subject: DS and Graph paths are not limited to existing files --- .../0002_alter_dashboard_groups_read_and_more.py | 123 +++++++++++++++++++++ rrd/models.py | 68 ++++++++---- rrd/tests/test_datasources.py | 8 ++ rrd/tests/test_graphs.py | 9 ++ 4 files changed, 189 insertions(+), 19 deletions(-) create mode 100644 rrd/migrations/0002_alter_dashboard_groups_read_and_more.py diff --git a/rrd/migrations/0002_alter_dashboard_groups_read_and_more.py b/rrd/migrations/0002_alter_dashboard_groups_read_and_more.py new file mode 100644 index 0000000..bc1c863 --- /dev/null +++ b/rrd/migrations/0002_alter_dashboard_groups_read_and_more.py @@ -0,0 +1,123 @@ +# Generated by Django 4.2.9 on 2024-01-27 10:24 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("auth", "0012_alter_user_first_name_max_length"), + ("rrd", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="dashboard", + name="groups_read", + field=models.ManyToManyField( + related_name="%(class)s_read", to="auth.group" + ), + ), + migrations.AlterField( + model_name="dashboard", + name="groups_write", + field=models.ManyToManyField( + related_name="%(class)s_write", to="auth.group" + ), + ), + migrations.AlterField( + model_name="dashboard", + name="users_read", + field=models.ManyToManyField( + related_name="%(class)s_read", to=settings.AUTH_USER_MODEL + ), + ), + migrations.AlterField( + model_name="dashboard", + name="users_write", + field=models.ManyToManyField( + related_name="%(class)s_write", to=settings.AUTH_USER_MODEL + ), + ), + migrations.AlterField( + model_name="datasource", + name="groups_read", + field=models.ManyToManyField( + related_name="%(class)s_read", to="auth.group" + ), + ), + migrations.AlterField( + model_name="datasource", + name="groups_write", + field=models.ManyToManyField( + related_name="%(class)s_write", to="auth.group" + ), + ), + migrations.AlterField( + model_name="datasource", + name="path", + field=models.CharField(max_length=512), + ), + migrations.AlterField( + model_name="datasource", + name="rrd_config", + field=models.TextField( + default="\nDS:{ds_name}:GAUGE:600:U:U\nRRA:AVERAGE:0.5:1:2016\nRRA:AVERAGE:0.5:12:720\nRRA:MAX:0.5:288:365\nRRA:MIN:0.5:288:365\n" + ), + ), + migrations.AlterField( + model_name="datasource", + name="users_read", + field=models.ManyToManyField( + related_name="%(class)s_read", to=settings.AUTH_USER_MODEL + ), + ), + migrations.AlterField( + model_name="datasource", + name="users_write", + field=models.ManyToManyField( + related_name="%(class)s_write", to=settings.AUTH_USER_MODEL + ), + ), + migrations.AlterField( + model_name="graph", + name="groups_read", + field=models.ManyToManyField( + related_name="%(class)s_read", to="auth.group" + ), + ), + migrations.AlterField( + model_name="graph", + name="groups_write", + field=models.ManyToManyField( + related_name="%(class)s_write", to="auth.group" + ), + ), + migrations.AlterField( + model_name="graph", + name="path", + field=models.CharField(max_length=512), + ), + migrations.AlterField( + model_name="graph", + name="rrd_config", + field=models.TextField( + default="\n--imgformat\n PNG\n--width\n 800\n--height\n 200\n--start\n -86400\n--end\n -1\n--vertical-label\n {ds_names[0]}\n--title\n {title}\n--right-axis\n 1:0\n--alt-autoscale\nDEF:{ds_names[0]}={ds_paths[0]}:{ds_names[0]}:AVERAGE\nVDEF:max={ds_names[0]},MAXIMUM\nVDEF:min={ds_names[0]},MINIMUM\nLINE2:{ds_names[0]}#0000FF\nLINE1:max#FF0000\nLINE1:min#FF0000\n" + ), + ), + migrations.AlterField( + model_name="graph", + name="users_read", + field=models.ManyToManyField( + related_name="%(class)s_read", to=settings.AUTH_USER_MODEL + ), + ), + migrations.AlterField( + model_name="graph", + name="users_write", + field=models.ManyToManyField( + related_name="%(class)s_write", to=settings.AUTH_USER_MODEL + ), + ), + ] diff --git a/rrd/models.py b/rrd/models.py index 3cace0c..2dc629a 100644 --- a/rrd/models.py +++ b/rrd/models.py @@ -1,5 +1,6 @@ import logging import os +import pathlib import django.contrib.auth.models as amodels import rrdtool @@ -9,6 +10,16 @@ from django.db import models log = logging.getLogger(__name__) +def _sanitize_path(path): + # changing all separators to a _ should result in an ugly filename + # that will not cause issues. + # an additional _ is added at the beginning to prevent hidden files + for sep in os.sep, os.altsep: + if sep: + path = path.replace(sep, "_") + return "_" + path + + class ModelWithPerms(models.Model): users_read = models.ManyToManyField( amodels.User, @@ -35,9 +46,7 @@ class DataSource(ModelWithPerms): # an mqtt topic can be as long as 65,535 bytes when UTF-8 encoded, # which is probably too much for a sensible db topic = models.CharField(max_length=512) - path = models.FilePathField( - path=settings.RRD_DB_PATH.as_posix(), - recursive=True, + path = models.CharField( max_length=512, ) rrd_config = models.TextField( @@ -50,27 +59,37 @@ class DataSource(ModelWithPerms): def __str__(self): return self.topic + @property + def rrd_path(self): + path = os.path.abspath(os.path.join( + settings.RRD_DB_PATH, self.path + )) + base_path = os.path.abspath(settings.RRD_DB_PATH) + if pathlib.Path(base_path) not in pathlib.Path(path).parents: + return os.path.abspath(os.path.join( + settings.RRD_DB_PATH, + _sanitize_path(self.path) + )) + return path + + @property + def ds_name(self): + return self.topic.split("/")[-1] + @property def lastupdate(self): try: - last = rrdtool.lastupdate(os.path.join( - settings.RRD_DB_PATH, self.path - )) + last = rrdtool.lastupdate(self.rrd_path) except rrdtool.OperationalError as e: log.warning("Failure reading from ds: %s", e) return (None, None) else: return last["date"], last["ds"][self.ds_name] - @property - def ds_name(self): - return self.topic.split("/")[-1] - def update(self, ts, value): - rrd_path = os.path.join(settings.RRD_DB_PATH, self.path) - if not os.path.isfile(rrd_path): + if not os.path.isfile(self.rrd_path): rrdtool.create( - os.path.join(settings.RRD_DB_PATH, self.path), + self.rrd_path, "--no-overwrite", self.rrd_config.format( ds_name=self.ds_name @@ -78,7 +97,7 @@ class DataSource(ModelWithPerms): ) try: rrdtool.update( - os.path.join(settings.RRD_DB_PATH, self.path), + self.rrd_path, str(ts) + ":" + str(value) ) except ValueError as e: @@ -91,9 +110,7 @@ class DataSource(ModelWithPerms): class Graph(ModelWithPerms): title = models.CharField(max_length=64) data_sources = models.ManyToManyField(DataSource) - path = models.FilePathField( - path=settings.RRD_GRAPH_PATH.as_posix(), - recursive=True, + path = models.CharField( max_length=512, ) rrd_config = models.TextField( @@ -103,14 +120,27 @@ class Graph(ModelWithPerms): def __str__(self): return self.title + @property + def graph_path(self): + path = os.path.abspath(os.path.join( + settings.RRD_GRAPH_PATH, self.path + )) + base_path = os.path.abspath(settings.RRD_GRAPH_PATH) + if pathlib.Path(base_path) not in pathlib.Path(path).parents: + return os.path.abspath(os.path.join( + settings.RRD_GRAPH_PATH, + _sanitize_path(self.path) + )) + return path + def update(self): - graph_path = os.path.join(settings.RRD_GRAPH_PATH, self.path) + graph_path = self.graph_path os.makedirs(os.path.dirname(graph_path), exist_ok=True) rrd_paths = [] rrd_topics = [] rrd_ds_names = [] for ds in self.data_sources.all(): - rrd_paths.append(os.path.join(settings.RRD_DB_PATH, ds.path)) + rrd_paths.append(ds.rrd_path) rrd_topics.append(ds.topic) rrd_ds_names.append(ds.ds_name) opts = self.rrd_config.format( diff --git a/rrd/tests/test_datasources.py b/rrd/tests/test_datasources.py index 4057b14..030d0ae 100644 --- a/rrd/tests/test_datasources.py +++ b/rrd/tests/test_datasources.py @@ -50,3 +50,11 @@ class TestDataSource(TestCase): last = ds.lastupdate self.assertEqual(last[1], 10) self.assertEqual(last[0].year, now.year) + + def test_invalid_path(self): + ds = models.DataSource.objects.create( + topic="evil", + path="../../../evil.rrd" + ) + self.assertTrue(ds.rrd_path.endswith("_.._.._.._evil.rrd")) + self.assertIn(settings.RRD_DB_PATH.as_posix(), ds.rrd_path) diff --git a/rrd/tests/test_graphs.py b/rrd/tests/test_graphs.py index f2a4948..7a4dd13 100644 --- a/rrd/tests/test_graphs.py +++ b/rrd/tests/test_graphs.py @@ -37,3 +37,12 @@ class TestGraphs(TestCase): ds.update(ts, 10) stat = os.stat(os.path.join(settings.RRD_GRAPH_PATH, "test/test.png")) self.assertGreaterEqual(stat.st_mtime, now.timestamp()) + + def test_invalid_path(self): + graph = models.Graph.objects.create( + title="Test Graph", + path="../../../etc/evil.png", + ) + + self.assertTrue(graph.graph_path.endswith("_.._.._.._etc_evil.png")) + self.assertIn(settings.RRD_GRAPH_PATH.as_posix(), graph.graph_path) -- cgit v1.2.3