Index: trac/env.py
===================================================================
--- trac/env.py	(revision 4119)
+++ trac/env.py	(working copy)
@@ -373,7 +373,11 @@
         return self._abs_href
     abs_href = property(_get_abs_href, 'The application URL')
 
+    def _get_form_secret(self):
+        return str(os.stat(os.path.join(self.path, 'VERSION')).st_ctime)
+    form_secret = property(_get_form_secret)
 
+
 class EnvironmentSetup(Component):
     implements(IEnvironmentSetupParticipant)
 
Index: trac/web/chrome.py
===================================================================
--- trac/web/chrome.py	(revision 4119)
+++ trac/web/chrome.py	(working copy)
@@ -432,6 +432,7 @@
             'href': req and req.href,
             'perm': req and req.perm,
             'authname': req and req.authname or '<trac>',
+            'form_token': req.form_token,
 
             # Date/time formatting
             'format_datetime': partial(format_datetime, tzinfo=tzinfo),
Index: trac/web/main.py
===================================================================
--- trac/web/main.py	(revision 4119)
+++ trac/web/main.py	(working copy)
@@ -16,6 +16,7 @@
 # Author: Christopher Lenz <cmlenz@gmx.de>
 #         Matthew Good <trac@matt-good.net>
 
+import sha
 import locale
 import os
 import sys
@@ -173,7 +174,8 @@
             'hdf': self._get_hdf,
             'perm': self._get_perm,
             'session': self._get_session,
-            'tz': self._get_timezone
+            'tz': self._get_timezone,
+            'form_token': self._get_form_token
         })
 
         # Select the component that should handle the request
@@ -193,6 +195,10 @@
         req.callbacks['chrome'] = partial(chrome.prepare_request,
                                           handler=chosen_handler)
 
+        if (req.method == 'POST' and
+            req.args.get('__form_token__') != req.form_token):
+            raise TracError('Missing or invalid form token')
+
         # Process the request and render the template
         try:
             try:
@@ -251,6 +257,9 @@
         except:
             return localtz
 
+    def _get_form_token(self, req):
+        return sha.sha(self.env.form_secret + req.authname).hexdigest()
+
     def _pre_process_request(self, req, chosen_handler):
         for filter_ in self.filters:
             chosen_handler = filter_.pre_process_request(req, chosen_handler)
Index: templates/layout.html
===================================================================
--- templates/layout.html	(revision 4119)
+++ templates/layout.html	(working copy)
@@ -80,6 +80,12 @@
     </div>
   </body>
 
+  <form py:match="form[@method='post' and @avoidloop!='true']" 
+        avoidloop="true" py:attrs="select('@*')">
+    <input type="hidden" name="__form_token__" value="$form_token"/>
+    ${select('*|text()')}
+  </form>
+
   <xi:include href="site.html"><xi:fallback /></xi:include>
 
 </html>

