Skip to content

Commit ae67928

Browse files
committed
editable packages: only allow reading or editing files inside the package's directory
This commit adds validation on views which read, write or delete files belonging to extensions and themes.
1 parent 5e5ac4e commit ae67928

File tree

5 files changed

+67
-17
lines changed

5 files changed

+67
-17
lines changed

editor/forms.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,18 @@ def clean_zipfile(self):
278278
raise forms.ValidationError('Uploaded file is not a zip file')
279279
return value
280280

281+
class PackageFileFormMixin:
282+
def clean(self):
283+
cleaned_data = super().clean()
284+
package = self.instance
285+
filename = cleaned_data.get('filename')
286+
package_path = Path(package.extracted_path).resolve()
287+
path = (package_path / filename).resolve()
288+
if not path.is_relative_to(package_path):
289+
raise forms.ValidationError("This file is not in the package's directory.")
290+
return cleaned_data
281291

282-
class EditPackageForm(forms.ModelForm):
292+
class EditPackageForm(PackageFileFormMixin, forms.ModelForm):
283293

284294
"""Form to edit a file in a package."""
285295

@@ -299,7 +309,7 @@ def save(self, commit=True):
299309
f.write(self.cleaned_data.get('source'))
300310
return package
301311

302-
class EditPackageReplaceFileForm(forms.ModelForm):
312+
class EditPackageReplaceFileForm(PackageFileFormMixin, forms.ModelForm):
303313

304314
"""Form to replace a file in a package."""
305315

@@ -330,7 +340,7 @@ class ReplaceExtensionFileForm(EditPackageReplaceFileForm):
330340
class Meta(EditPackageReplaceFileForm.Meta):
331341
model = Extension
332342

333-
class PackageDeleteFileForm(forms.ModelForm):
343+
class PackageDeleteFileForm(PackageFileFormMixin, forms.ModelForm):
334344
filename = forms.CharField(widget=forms.HiddenInput)
335345

336346
class Meta:

editor/templates/editable_package/edit_base.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ <h1 class="name-header">
3535
{% endif %}
3636
</ul>
3737
{% if object.editable %}
38-
<div class="panel-heading">Files {{filename}}</div>
38+
<div class="panel-heading">Files</div>
3939
<ul class="list-group">
4040
{% if parent_directory != current_directory %}
4141
<div class="list-group-item parent-directories">
@@ -49,8 +49,8 @@ <h1 class="name-header">
4949
</nav>
5050
</div>
5151
{% endif %}
52-
{% for fname, is_dir in filenames %}
53-
<a class="list-group-item monospace{% if fname == filename %} active{% endif %}{% if is_dir %} dir{% endif %}" href="{% package_url 'edit_source' object.pk %}?filename={{fname}}">{{fname}}</a>
52+
{% for absfname, relfname, is_dir in filenames %}
53+
<a class="list-group-item monospace{% if absfname == filename %} active{% endif %}{% if is_dir %} dir{% endif %}" href="{% package_url 'edit_source' object.pk %}?filename={{absfname}}">{{relfname}}</a>
5454
{% endfor %}
5555

5656
{% if editable %}

editor/templates/editable_package/edit_source.html

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,22 +57,32 @@
5757
{% endif %}
5858
</div>
5959

60+
<form action="{% package_url 'edit_source' form.instance.pk %}" method="POST" id="save-file">
6061
{% if is_image %}
6162
{% if exists %}
6263
<a href="{{file_url}}" download="{{filename_without_directories}}"><img class="image-file thumbnail" src="{{file_url}}"></a>
6364
{% endif %}
6465
{% else %}
6566
{% if not is_binary %}
66-
<form action="{% package_url 'edit_source' form.instance.pk %}" method="POST" id="save-file">
67-
{% csrf_token %}
68-
{% for field in form %}
69-
{{field}}
67+
{% if form.errors %}
68+
{% for error_group in form.errors.values %}
69+
{% for error in error_group %}
70+
<div class="alert alert-danger">
71+
<p>{{error}}</p>
72+
</div>
73+
{% endfor %}
7074
{% endfor %}
71-
</form>
75+
{% endif %}
76+
77+
{% csrf_token %}
78+
{% for field in form %}
79+
{{field}}
80+
{% endfor %}
7281
{% else %}
7382
<p>This is a binary file.</p>
7483
{% endif %}
7584
{% endif %}
85+
</form>
7686
{% endif %}
7787
{% endblock package_edit_content %}
7888

editor/views/editable_package.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django import http
1010
from django.contrib import messages
1111
from django.contrib.contenttypes.models import ContentType
12+
from django.core.exceptions import PermissionDenied
1213
from django.views import generic
1314
from django.urls import reverse
1415
from django.utils.timezone import make_aware
@@ -33,15 +34,25 @@ def get_context_data(self,**kwargs):
3334

3435
package = self.get_object()
3536

36-
context['filenames'] = [(x, (self.get_object().extracted_path / x).is_dir()) for x in self.get_package_filenames()]
37+
package_path = Path(package.extracted_path).resolve()
38+
39+
current_dir = (package_path / self.get_current_directory()).resolve()
40+
41+
# For a given path, return a triple: (path relative to package root, path relative to current directory, is a directory?)
42+
def path_info(path):
43+
abspath = (package_path / path).resolve()
44+
return (abspath.relative_to(package_path), abspath.relative_to(current_dir), abspath.is_dir())
45+
46+
context['filenames'] = [path_info(x) for x in self.get_package_filenames()]
3747
context['editable'] = self.package.can_be_edited_by(self.request.user)
3848
context['upload_file_form'] = self.upload_file_form_class(instance=self.get_object())
3949

4050
filename = self.get_filename()
51+
4152
if filename is not None:
4253
path = context['path'] = self.get_path()
4354
current_directory = context['current_directory'] = self.get_current_directory().relative_to(package.extracted_path)
44-
context['filename'] = path.relative_to(package.extracted_path)
55+
context['filename'] = (package_path / path).resolve().relative_to(package_path)
4556
context['parent_directory'] = current_directory.parent
4657

4758
context['exists'] = path.exists()
@@ -72,10 +83,14 @@ def get_filename(self):
7283

7384
def get_path(self):
7485
package = self.get_object()
75-
return Path(package.extracted_path) / self.get_filename()
86+
package_path = Path(package.extracted_path)
87+
path = package_path / self.get_filename()
88+
if not path.is_relative_to(package_path):
89+
raise PermissionDenied("This file is not in the package's directory.")
90+
return path
7691

7792
def get_current_directory(self):
78-
path = self.get_path()
93+
path = self.get_path().resolve()
7994
if path.is_dir():
8095
return path
8196
else:
@@ -93,12 +108,15 @@ def __init__(self,*args,**kwargs):
93108
super().__init__(*args,**kwargs)
94109

95110
def dispatch(self,request,*args,**kwargs):
96-
package = self.get_object()
111+
package = self.package = self.get_object()
97112
if not package.editable:
98113
return forbidden_response(self.request,"This package is not editable.")
99114
return super().dispatch(request,*args,**kwargs)
100115

101116
def load_source(self,path):
117+
if not path.resolve().is_relative_to(self.package.extracted_path):
118+
raise PermissionDenied(f"This file is not in the package's directory.")
119+
102120
if path.is_dir():
103121
return None
104122
try:
@@ -135,8 +153,14 @@ def get_context_data(self, **kwargs):
135153
path = context['path'] = self.get_path()
136154

137155
filenames = context['filenames']
156+
157+
package_path = Path(package.extracted_path)
158+
159+
current_dir = (package_path / self.get_current_directory()).resolve()
160+
138161
if not context['exists']:
139-
filenames.append((self.get_current_directory() / filename, False))
162+
filenames.append((Path(filename), (package_path / filename).resolve().relative_to(current_dir), False))
163+
140164
filenames.sort()
141165

142166
context['is_binary'] = self.is_binary
@@ -194,6 +218,9 @@ def get_success_url(self):
194218

195219
class AccessView(ShowPackageFilesMixin, AuthorRequiredMixin, generic.UpdateView):
196220
template_name = 'editable_package/access.html'
221+
222+
def get_filename(self):
223+
return None
197224

198225
def get_form_kwargs(self, *args, **kwargs):
199226
kwargs = super().get_form_kwargs(*args,**kwargs)

editor/views/theme.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ def get_filename(self):
2323
return self.place_filename(filename)
2424

2525
def place_filename(self, filename):
26+
if filename is None:
27+
return None
28+
2629
extension_dirs = {
2730
'.md': '',
2831
'.txt': '',

0 commit comments

Comments
 (0)