comparison src/server/server.py @ 956:dabecab10faa

server (pages): impleted CSRF protection: A basic CSRF protection has been implemented using CSRF token. The token is created on session creation, and checked on data post. The process should be fully automatic, and a hidden field is added in forms in sat_templates when csrf_token is present in template data (require to import input/form.html with context). If token is wrong on absent, an unauthorized error page is returned (and client ip is logged). Also don't use anymore inlineCallbacks in _on_data_post, as StopIteration exception are catched by inlineCallbacks, resulting in bad behaviour. As a further security, getPostedDate raise a KeyError instead of StopIteration is a specific key is looked for and missing. Added HTTP_SEE_OTHER status code in constants.
author Goffi <goffi@goffi.org>
date Mon, 10 Jul 2017 19:10:31 +0200
parents 4f7cb6335a33
children 67bf14c91d5c
comparison
equal deleted inserted replaced
955:4f7cb6335a33 956:dabecab10faa
1688 log.error(_(u"Uncatched error for HTTP request on {url}: {msg}").format( 1688 log.error(_(u"Uncatched error for HTTP request on {url}: {msg}").format(
1689 url = request.URLPath(), 1689 url = request.URLPath(),
1690 msg = failure_)) 1690 msg = failure_))
1691 self.pageError(request, C.HTTP_INTERNAL_ERROR) 1691 self.pageError(request, C.HTTP_INTERNAL_ERROR)
1692 1692
1693 @defer.inlineCallbacks 1693 def _on_data_post_redirect(self, ret, request):
1694 def _on_data_post(self, dummy, request): 1694 """called when page's on_data_post has been called successfuly
1695 yield defer.maybeDeferred(self.on_data_post, self, request) 1695
1696 request.setResponseCode(303) 1696 this method redirect to the same page, using Post/Redirect/Get pattern
1697 HTTP status code "See Other" (303) is the recommanded code in this case
1698 """
1699 request.setResponseCode(C.HTTP_SEE_OTHER)
1697 request.setHeader("location", request.uri) 1700 request.setHeader("location", request.uri)
1698 request.finish() 1701 request.finish()
1699 raise failure.Failure(exceptions.CancelError(u'Post/Redirect/Get is used')) 1702 raise failure.Failure(exceptions.CancelError(u'Post/Redirect/Get is used'))
1703
1704 def _on_data_post(self, dummy, request):
1705 csrf_token = self.host.getSessionData(request, session_iface.ISATSession).csrf_token
1706 try:
1707 given_csrf = self.getPostedData(request, u'csrf_token')
1708 except KeyError:
1709 given_csrf = None
1710 if given_csrf is None or given_csrf != csrf_token:
1711 log.warning(_(u"invalid CSRF token, hack attempt? URL: {url}, IP: {ip}").format(
1712 url=request.uri,
1713 ip=request.getClientIP()))
1714 self.pageError(request, C.HTTP_UNAUTHORIZED)
1715 d = defer.maybeDeferred(self.on_data_post, self, request)
1716 d.addCallback(self._on_data_post_redirect, request)
1717 return d
1700 1718
1701 1719
1702 def getPostedData(self, request, keys, multiple=False): 1720 def getPostedData(self, request, keys, multiple=False):
1703 """get data from a POST request and decode it 1721 """get data from a POST request and decode it
1704 1722
1707 unicode to get one value 1725 unicode to get one value
1708 iterable to get more than one 1726 iterable to get more than one
1709 @param multiple(bool): True if multiple values are possible/expected 1727 @param multiple(bool): True if multiple values are possible/expected
1710 if False, the first value is returned 1728 if False, the first value is returned
1711 @return (iterator[unicode], list[iterator[unicode], unicode, list[unicode]): values received for this(these) key(s) 1729 @return (iterator[unicode], list[iterator[unicode], unicode, list[unicode]): values received for this(these) key(s)
1730 @raise KeyError: one specific key has been requested, and it is missing
1712 """ 1731 """
1713 if isinstance(keys, basestring): 1732 if isinstance(keys, basestring):
1714 keys = [keys] 1733 keys = [keys]
1715 get_first = True 1734 get_first = True
1716 else: 1735 else:
1720 for key in keys: 1739 for key in keys:
1721 gen = (urllib.unquote(v).decode('utf-8') for v in request.args.get(key,[])) 1740 gen = (urllib.unquote(v).decode('utf-8') for v in request.args.get(key,[]))
1722 if multiple: 1741 if multiple:
1723 ret.append(gen) 1742 ret.append(gen)
1724 else: 1743 else:
1725 ret.append(next(gen)) 1744 try:
1745 ret.append(next(gen))
1746 except StopIteration:
1747 return KeyError(key)
1726 1748
1727 return ret[0] if get_first else ret 1749 return ret[0] if get_first else ret
1728 1750
1729 def getProfile(self, request): 1751 def getProfile(self, request):
1730 """helper method to easily get current profile 1752 """helper method to easily get current profile
1766 1788
1767 def renderPage(self, request, skip_parse_url=False): 1789 def renderPage(self, request, skip_parse_url=False):
1768 """Main method to handle the workflow of a LiberviaPage""" 1790 """Main method to handle the workflow of a LiberviaPage"""
1769 # template_data are the variables passed to template 1791 # template_data are the variables passed to template
1770 if not hasattr(request, 'template_data'): 1792 if not hasattr(request, 'template_data'):
1771 request.template_data = {} 1793 csrf_token = self.host.getSessionData(request, session_iface.ISATSession).csrf_token
1794 request.template_data = {'csrf_token': csrf_token}
1772 1795
1773 # XXX: here is the code which need to be executed once 1796 # XXX: here is the code which need to be executed once
1774 # at the beginning of the request hanling 1797 # at the beginning of the request hanling
1775 if request.postpath and not request.postpath[-1]: 1798 if request.postpath and not request.postpath[-1]:
1776 # we don't differenciate URLs finishing with '/' or not 1799 # we don't differenciate URLs finishing with '/' or not