Skip to content

Commit 04cae7a

Browse files
authored
Prevent user comments xss (#225)
* feat: Prevent XSS attack from comments - Escaped comment text - Fixed SQL Linter test comment text because of escaping characters - Added a unittest for testing escaping html in comment text - Fixed a linter test
1 parent 573d654 commit 04cae7a

File tree

5 files changed

+46
-20
lines changed

5 files changed

+46
-20
lines changed

lms/lmsdb/models.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from collections import Counter
22
import enum
3+
import html
34
import secrets
45
import string
56
from datetime import datetime
@@ -708,7 +709,7 @@ def create_comment(
708709
cls, text: str, flake_key: Optional[str] = None,
709710
) -> 'CommentText':
710711
instance, _ = CommentText.get_or_create(
711-
**{CommentText.text.name: text},
712+
**{CommentText.text.name: html.escape(text)},
712713
defaults={CommentText.flake8_key.name: flake_key},
713714
)
714715
return instance

lms/tests/test_flake8_linter.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import shutil
33
import tempfile
44

5-
from lms.lmsdb import models
5+
from lms.lmsdb.models import Comment, Solution
66
from lms.lmstests.public.linters import tasks
77
from lms.models import notifications
88

@@ -30,23 +30,23 @@ def teardown_class(cls):
3030
if cls.test_directory is not None:
3131
shutil.rmtree(cls.test_directory)
3232

33-
def test_pyflake_wont_execute_code(self, solution: models.Solution):
33+
def test_pyflake_wont_execute_code(self, solution: Solution):
3434
solution_file = solution.solution_files.get()
3535
solution_file.code = self.execute_script
3636
solution_file.save()
3737
tasks.run_linter_on_solution(solution.id)
38-
comments = tuple(models.Comment.by_solution(solution))
38+
comments = tuple(Comment.by_solution(solution))
3939
assert not os.listdir(self.test_directory)
4040
assert len(comments) == 2
4141
exec(compile(self.execute_script, '', 'exec')) # noqa S102
4242
assert os.listdir(self.test_directory) == ['some-file']
4343

44-
def test_invalid_solution(self, solution: models.Solution):
44+
def test_invalid_solution(self, solution: Solution):
4545
solution_file = solution.solution_files.get()
4646
solution_file.code = INVALID_CODE
4747
solution_file.save()
4848
tasks.run_linter_on_solution(solution.id)
49-
comments = tuple(models.Comment.by_solution(solution))
49+
comments = tuple(Comment.by_solution(solution))
5050
assert comments
5151
assert len(comments) == 1
5252
comment = comments[0].comment
@@ -59,10 +59,10 @@ def test_invalid_solution(self, solution: models.Solution):
5959
assert solution.exercise.subject in subject
6060
assert '1' in subject
6161

62-
def test_valid_solution(self, solution: models.Solution):
62+
def test_valid_solution(self, solution: Solution):
6363
solution_file = solution.solution_files.get()
6464
solution_file.code = VALID_CODE
6565
solution_file.save()
6666
tasks.run_linter_on_solution(solution.id)
67-
comments = tuple(models.Comment.by_solution(solution))
67+
comments = tuple(Comment.by_solution(solution))
6868
assert not comments

lms/tests/test_html_escaping.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from flask import json
2+
3+
from lms.lmsdb.models import Solution, User
4+
from lms.tests import conftest
5+
6+
7+
USER_COMMENT_BEFORE_ESCAPING = '<html><body><p>Welcome "LMS"</p></body></html>'
8+
USER_COMMENT_AFTER_ESCAPING = (
9+
'&lt;html&gt;&lt;body&gt;&lt;p&gt;Welcome &quot;LMS&quot;'
10+
'&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;'
11+
)
12+
13+
14+
class TestHtmlEscaping:
15+
@staticmethod
16+
def test_comment_text_escaping(student_user: User, solution: Solution):
17+
client = conftest.get_logged_user(student_user.username)
18+
19+
# Creating a comment
20+
comment_response = client.post('/comments', data=json.dumps(dict(
21+
fileId=solution.files[0].id, act='create', kind='text',
22+
comment=USER_COMMENT_BEFORE_ESCAPING, line=1,
23+
)), content_type='application/json')
24+
assert comment_response.status_code == 200
25+
assert solution.comments[0].comment.text == USER_COMMENT_AFTER_ESCAPING

lms/tests/test_html_linter.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
import pytest
44

5-
from lms.lmsdb import models
5+
from lms.lmsdb.models import Comment, Solution
66
from lms.lmstests.public.linters import tasks
77

88

99
INVALID_CODE = '<html>'
1010
INVALID_CODE_MESSAGES = {
11-
'Start tag seen without seeing a doctype first. Expected “<!DOCTYPE html>”.',
11+
'Start tag seen without seeing a doctype first. Expected “&lt;!DOCTYPE html&gt;”.',
1212
'Element “head” is missing a required instance of child element “title”.',
1313
'Consider adding a “lang” attribute to the “html” start tag to declare the language of this document.',
1414
}
@@ -31,23 +31,23 @@
3131
reason='should run with VNU linter in path. see VNULinter class for more information',
3232
)
3333
class TestHTMLLinter:
34-
def test_invalid_solution(self, solution: models.Solution):
34+
def test_invalid_solution(self, solution: Solution):
3535
solution_file = solution.solution_files.get()
3636
solution_file.path = 'index.html'
3737
solution_file.code = INVALID_CODE
3838
solution_file.save()
3939
tasks.run_linter_on_solution(solution.id)
40-
comments = tuple(models.Comment.by_solution(solution))
40+
comments = tuple(Comment.by_solution(solution))
4141
assert comments
4242
assert len(comments) == 3
4343
comment_texts = {comment.comment.text for comment in comments}
4444
assert comment_texts == INVALID_CODE_MESSAGES
4545

46-
def test_valid_solution(self, solution: models.Solution):
46+
def test_valid_solution(self, solution: Solution):
4747
solution_file = solution.solution_files.get()
4848
solution_file.path = 'index.html'
4949
solution_file.code = VALID_CODE
5050
solution_file.save()
5151
tasks.run_linter_on_solution(solution.id)
52-
comments = tuple(models.Comment.by_solution(solution))
52+
comments = tuple(Comment.by_solution(solution))
5353
assert not comments

lms/tests/test_sql_linter.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
1-
from lms.lmsdb import models
1+
from lms.lmsdb.models import Comment, Solution
22
from lms.lmstests.public.linters import tasks
33

44

55
INVALID_CODE = 's1\n'
6-
INVALID_CODE_MESSAGE = "Found unparsable section: 's1'"
6+
INVALID_CODE_MESSAGE = 'Found unparsable section: &#x27;s1&#x27;' # Escape
77

88
VALID_CODE = 'SELECT 1\n'
99

1010

1111
class TestSQLLinter:
12-
def test_invalid_solution(self, solution: models.Solution):
12+
def test_invalid_solution(self, solution: Solution):
1313
solution_file = solution.solution_files.get()
1414
solution_file.path = 'sql.sql'
1515
solution_file.code = INVALID_CODE
1616
solution_file.save()
1717
tasks.run_linter_on_solution(solution.id)
18-
comments = tuple(models.Comment.by_solution(solution))
18+
comments = tuple(Comment.by_solution(solution))
1919
assert comments
2020
assert len(comments) == 1
2121
assert comments[0].comment.text == INVALID_CODE_MESSAGE
2222

23-
def test_valid_solution(self, solution: models.Solution):
23+
def test_valid_solution(self, solution: Solution):
2424
solution_file = solution.solution_files.get()
2525
solution_file.path = 'sql.sql'
2626
solution_file.code = VALID_CODE
2727
solution_file.save()
2828
tasks.run_linter_on_solution(solution.id)
29-
comments = tuple(models.Comment.by_solution(solution))
29+
comments = tuple(Comment.by_solution(solution))
3030
assert not comments

0 commit comments

Comments
 (0)