diff --git a/precise_bbcode/bbcode/defaults/placeholder.py b/precise_bbcode/bbcode/defaults/placeholder.py
index 7bc21ad..6004f3c 100644
--- a/precise_bbcode/bbcode/defaults/placeholder.py
+++ b/precise_bbcode/bbcode/defaults/placeholder.py
@@ -3,9 +3,9 @@
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
+from precise_bbcode.conf import settings as bbcode_settings
from precise_bbcode.bbcode.placeholder import BBCodePlaceholder
-
__all__ = [
'UrlBBCodePlaceholder',
'EmailBBCodePlaceholder',
@@ -30,11 +30,17 @@ class UrlBBCodePlaceholder(BBCodePlaceholder):
name = 'url'
def validate(self, content, extra_context=None):
- v = URLValidator()
- try:
- v(content)
- except ValidationError:
+ for xss in bbcode_settings.URL_XSS_FILTER:
+ if xss in content:
+ return False
+ if content[:2] == '//':
return False
+ if '://' in content:
+ v = URLValidator()
+ try:
+ v(content)
+ except ValidationError:
+ return False
return True
diff --git a/precise_bbcode/bbcode/defaults/tag.py b/precise_bbcode/bbcode/defaults/tag.py
index ca202c7..caedc27 100644
--- a/precise_bbcode/bbcode/defaults/tag.py
+++ b/precise_bbcode/bbcode/defaults/tag.py
@@ -104,27 +104,41 @@ class Options:
replace_links = False
def render(self, value, option=None, parent=None):
+ def bad_value_render(href):
+ if option:
+ return '[url={}]{}[/url]'.format(href, value)
+ else:
+ return '[url]{}[/url]'.format(value)
+
href = option if option else value
if href[0] == href[-1] and href[0] in ('"', '\'') and len(href) > 2:
# URLs can be encapsulated in quotes (either single or double) that aren't part of the
# URL. If that's the case, strip them out.
href = href[1:-1]
+ if not href:
+ return bad_value_render(href)
href = replace(href, bbcode_settings.BBCODE_ESCAPE_HTML)
- if '://' not in href and self._domain_re.match(href):
- href = 'http://' + href
- v = URLValidator()
-
- # Validates and renders the considered URL.
- try:
- v(href)
- except ValidationError:
- rendered = '[url={}]{}[/url]'.format(href, value) if option else \
- '[url]{}[/url]'.format(value)
- else:
- content = value if option else href
- rendered = '{}'.format(href, content or href)
+ for xss in bbcode_settings.URL_XSS_FILTER:
+ if xss in href:
+ return bad_value_render(href)
- return rendered
+ if '://' not in href and self._domain_re.match(href):
+ href = 'https://' + href
+
+ if href[:2] == '//':
+ # Protocolless absolute URLs are unsafe.
+ return bad_value_render(href)
+
+ if '://' in href:
+ # Validates the considered URL only if it is not relative.
+ v = URLValidator()
+ try:
+ v(href)
+ except ValidationError:
+ return bad_value_render(href)
+
+ content = value if option else href
+ return '{}'.format(href, content or href)
class ImgBBCodeTag(BBCodeTag):
diff --git a/precise_bbcode/bbcode/parser.py b/precise_bbcode/bbcode/parser.py
index 96bf6e7..57b945c 100644
--- a/precise_bbcode/bbcode/parser.py
+++ b/precise_bbcode/bbcode/parser.py
@@ -1,6 +1,8 @@
import re
from collections import defaultdict
+from django.core.validators import URLValidator
+
from precise_bbcode.bbcode.regexes import url_re
from precise_bbcode.conf import settings as bbcode_settings
from precise_bbcode.core.utils import replace
@@ -402,8 +404,19 @@ def _render_textual_content(self, data, replace_specialchars, replace_links, rep
if replace_links:
def linkrepl(match):
url = match.group(0)
- href = url if '://' in url else 'http://' + url
- return '{1}'.format(href, url)
+ for xss in bbcode_settings.URL_XSS_FILTER:
+ if xss in url:
+ return url
+ if url[:2] == '//':
+ return url
+ if '://' in url:
+ v = URLValidator()
+ try:
+ v(url)
+ except ValidationError:
+ return url
+
+ return '{1}'.format(url, url)
data = re.sub(url_re, linkrepl, data)
if replace_smilies:
diff --git a/precise_bbcode/conf/settings.py b/precise_bbcode/conf/settings.py
index db74e24..473b863 100644
--- a/precise_bbcode/conf/settings.py
+++ b/precise_bbcode/conf/settings.py
@@ -30,3 +30,5 @@
# Smileys options
BBCODE_ALLOW_SMILIES = getattr(settings, 'BBCODE_ALLOW_SMILIES', True)
SMILIES_UPLOAD_TO = getattr(settings, 'BBCODE_SMILIES_UPLOAD_TO', 'precise_bbcode/smilies')
+
+URL_XSS_FILTER = '"<>^`{|}();'
diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py
index c9fd2ac..0829dce 100644
--- a/tests/unit/test_parser.py
+++ b/tests/unit/test_parser.py
@@ -32,23 +32,25 @@ class TestParser(object):
'[url]http://www.google.com[/url]',
'http://www.google.com'
),
- ('[url=google.com]goto google[/url]', 'goto google'),
+ ('[url=google.com]goto google[/url]', 'goto google'),
('[url=http://google.com][/url]', 'http://google.com'),
('[url=\'http://google.com\'][/url]', 'http://google.com'),
('[url="http://google.com"][/url]', 'http://google.com'),
- ('[URL=google.com]goto google[/URL]', 'goto google'),
+ ('[URL=google.com]goto google[/URL]', 'goto google'),
+ ('[URL=/localhost/www][/URL]', '/localhost/www'),
+ ('[URL=/localhost/www]goto localhost[/URL]', 'goto localhost'),
(
'[url=]xss[/url]',
'[url=<script>alert(1);</script>]xss[/url]'
),
(
'www.google.com foo.com/bar http://xyz.ci',
- 'www.google.com '
- 'foo.com/bar http://xyz.ci'
+ 'www.google.com '
+ 'foo.com/bar http://xyz.ci'
),
- ('[url=relative/foo/bar.html]link[/url]', '[url=relative/foo/bar.html]link[/url]'),
- ('[url=/absolute/foo/bar.html]link[/url]', '[url=/absolute/foo/bar.html]link[/url]'),
- ('[url=./hello.html]world![/url]', '[url=./hello.html]world![/url]'),
+ ('[url=relative/foo/bar.html]link[/url]', 'link'),
+ ('[url=/absolute/foo/bar.html]link[/url]', 'link'),
+ ('[url=./hello.html]world![/url]', 'world!'),
(
'[url=javascript:alert(String.fromCharCode(88,83,83))]http://google.com[/url]',
'[url=javascript:alert(String.fromCharCode(88,83,83))]http://google.com[/url]'
@@ -73,11 +75,19 @@ class TestParser(object):
'[img]http://foo.com/fake.png [img] '
'onerrorjavascript:alert(String.fromCharCode(88,83,83)) [/img] [/img]'
),
+ (
+ '[img]/localhost/www[/img]',
+ ''
+ ),
+ (
+ '[url=/localhost/www][img]/localhost/www[/img][/url]',
+ '
'
+ ),
('[quote] \r\nhello\nworld! [/quote]', '
hello'), ('[code][b]hello world![/b][/code]', '
world!
[b]hello world![/b]
'),
(
'[color=green]goto [url=google.com]google website[/url][/color]',
- 'goto google website'
+ 'goto google website'
),
('[color=#FFFFFF]white[/color]', 'white'),
(
@@ -142,8 +152,8 @@ class TestParser(object):
'definition_string': '[youtube]{TEXT}[/youtube]',
'format_string': (
''
),
@@ -208,7 +218,7 @@ class TestParser(object):
),
(
'[youtube]ztD3mRMdqSw[/youtube]',
- '' # noqa
+ '' # noqa
),
(
'[h1=#FFF]hello world![/h1]',