comparison roundup/cgi/templating.py @ 6282:d30501bafdfb

issue2551098: markdown links missing rel="noreferer nofollow" Links generated by all markdown backends are missing the noopener and nofollow relation that roundup's normal text -> html core adds to prevent security issues and link spam. Now rel="nofollow" is added to links generated by markdown2 backends and rel="nofollow noopener" for mistune and markdown backends. Markdown2 isn't as programable as the other two backends so I used the built-in nofollow support. This means that a user that generates a link that opens in a new window can manpulate the parent window.
author John Rouillard <rouilj@ieee.org>
date Sat, 31 Oct 2020 14:51:16 -0400
parents 6ed5152a92d0
children 3f7538316724
comparison
equal deleted inserted replaced
6281:042c50d5e06e 6282:d30501bafdfb
68 class Markdown(markdown2.Markdown): 68 class Markdown(markdown2.Markdown):
69 # don't allow disabled protocols in links 69 # don't allow disabled protocols in links
70 _safe_protocols = re.compile('(?!' + ':|'.join([re.escape(s) for s in _disable_url_schemes]) + ':)', re.IGNORECASE) 70 _safe_protocols = re.compile('(?!' + ':|'.join([re.escape(s) for s in _disable_url_schemes]) + ':)', re.IGNORECASE)
71 71
72 def _extras(config): 72 def _extras(config):
73 extras = { 'fenced-code-blocks' : {} } 73 extras = { 'fenced-code-blocks' : {}, 'nofollow': None }
74 if config['MARKDOWN_BREAK_ON_NEWLINE']: 74 if config['MARKDOWN_BREAK_ON_NEWLINE']:
75 extras['break-on-newline'] = True 75 extras['break-on-newline'] = True
76 return extras 76 return extras
77 77
78 markdown = lambda s, c: Markdown(safe_mode='escape', extras=_extras(c)).convert(s) 78 markdown = lambda s, c: Markdown(safe_mode='escape', extras=_extras(c)).convert(s)
94 url = el.attrib['href'].lstrip(' \r\n\t\x1a\0').lower() 94 url = el.attrib['href'].lstrip(' \r\n\t\x1a\0').lower()
95 for s in _disable_url_schemes: 95 for s in _disable_url_schemes:
96 if url.startswith(s + ':'): 96 if url.startswith(s + ':'):
97 el.attrib['href'] = '#' 97 el.attrib['href'] = '#'
98 98
99 class LinkRendererWithRel(Treeprocessor):
100 ''' Rendering class that sets the rel="nofollow noreferer"
101 for links. '''
102 rel_value = "nofollow noopener"
103
104 def run(self, root):
105 for el in root.iter('a'):
106 if 'href' in el.attrib:
107 url = el.get('href').lstrip(' \r\n\t\x1a\0').lower()
108 if not url.startswith('http'): # only add rel for absolute http url's
109 continue
110 el.set('rel', self.rel_value)
111
99 # make sure any HTML tags get escaped and some links restricted 112 # make sure any HTML tags get escaped and some links restricted
113 # and rel="nofollow noopener" are added to links
100 class SafeHtml(MarkdownExtension): 114 class SafeHtml(MarkdownExtension):
101 def extendMarkdown(self, md, md_globals=None): 115 def extendMarkdown(self, md, md_globals=None):
102 if hasattr(md.preprocessors, 'deregister'): 116 if hasattr(md.preprocessors, 'deregister'):
103 md.preprocessors.deregister('html_block') 117 md.preprocessors.deregister('html_block')
104 else: 118 else:
110 124
111 if hasattr(md.preprocessors, 'register'): 125 if hasattr(md.preprocessors, 'register'):
112 md.treeprocessors.register(RestrictLinksProcessor(), 'restrict_links', 0) 126 md.treeprocessors.register(RestrictLinksProcessor(), 'restrict_links', 0)
113 else: 127 else:
114 md.treeprocessors['restrict_links'] = RestrictLinksProcessor() 128 md.treeprocessors['restrict_links'] = RestrictLinksProcessor()
115 129 if hasattr(md.preprocessors, 'register'):
130 md.treeprocessors.register(LinkRendererWithRel(), 'add_link_rel', 0)
131 else:
132 md.treeprocessors['add_link_rel'] = LinkRendererWithRel()
133
116 def _extensions(config): 134 def _extensions(config):
117 extensions = [SafeHtml(), 'fenced_code'] 135 extensions = [SafeHtml(), 'fenced_code']
118 if config['MARKDOWN_BREAK_ON_NEWLINE']: 136 if config['MARKDOWN_BREAK_ON_NEWLINE']:
119 extensions.append('nl2br') 137 extensions.append('nl2br')
120 return extensions 138 return extensions
126 return markdown 144 return markdown
127 145
128 def _import_mistune(): 146 def _import_mistune():
129 try: 147 try:
130 import mistune 148 import mistune
149 from mistune import Renderer, escape_link, escape
150
131 mistune._scheme_blacklist = [ s + ':' for s in _disable_url_schemes ] 151 mistune._scheme_blacklist = [ s + ':' for s in _disable_url_schemes ]
132 152
153 class LinkRendererWithRel(Renderer):
154 ''' Rendering class that sets the rel="nofollow noreferer"
155 for links. '''
156
157 rel_value = "nofollow noopener"
158
159 def autolink(self, link, is_email=False):
160 ''' handle <url or email> style explicit links '''
161 text = link = escape_link(link)
162 if is_email:
163 link = 'mailto:%s' % link
164 return '<a href="%(href)s">%(text)s</a>' % { 'href': link, 'text': text }
165 return '<a href="%(href)s" rel="%(rel)s">%(href)s</a>' % {
166 'rel': self.rel_value, 'href': escape_link(link)}
167
168 def link(self, link, title, content):
169 ''' handle [text](url "title") style links and Reference
170 links '''
171
172 values = {
173 'content': escape(content),
174 'href': escape_link(link),
175 'rel': self.rel_value,
176 'title': escape(title) if title else '',
177 }
178
179 if title:
180 return '<a href="%(href)s" rel="%(rel)s" ' \
181 'title="%(title)s">%(content)s</a>' % values
182
183 return '<a href="%(href)s" rel="%(rel)s">%(content)s</a>' % values
184
133 def _options(config): 185 def _options(config):
134 options = {} 186 options = {'renderer': LinkRendererWithRel(escape = True)}
135 if config['MARKDOWN_BREAK_ON_NEWLINE']: 187 if config['MARKDOWN_BREAK_ON_NEWLINE']:
136 options['hard_wrap'] = True 188 options['hard_wrap'] = True
137 return options 189 return options
138 190
139 markdown = lambda s, c: mistune.markdown(s, **_options(c)) 191 markdown = lambda s, c: mistune.markdown(s, **_options(c))

Roundup Issue Tracker: http://roundup-tracker.org/