Mercurial > p > roundup > code
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)) |
