Index: trac/web/chrome.py
===================================================================
--- trac/web/chrome.py	(revision 4125)
+++ 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 4125)
+++ trac/web/main.py	(working copy)
@@ -173,7 +173,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 +194,15 @@
         req.callbacks['chrome'] = partial(chrome.prepare_request,
                                           handler=chosen_handler)
 
+        # Protect against CSRF attacks.
+        # We can only block against suck attacks if the user is logged in
+        # or if we have an incoming session cookie.
+        if (req.method == 'POST' and
+            req.args.get('__FORM_TOKEN') != req.form_token and
+            (req.incookie.has_key('trac_auth') or
+             req.incookie.has_key('trac_session'))):
+            raise TracError('Missing or invalid form token')
+
         # Process the request and render the template
         try:
             try:
@@ -251,6 +261,21 @@
         except:
             return localtz
 
+    def _get_form_token(self, req):
+        """Used to protect against CSRF.
+
+        The 'trac_auth' cookie is a good and strong shared secret, only
+        known by the user it belongs to and Trac itself.
+
+        The session id is our second best option, not as reliable since
+        it will change on each request if the user has cookies disabled in
+        his/her browser.
+        """
+        if req.incookie.has_key('trac_auth'):
+            return req.incookie['trac_auth'].value
+        else:
+            return req.session.sid
+
     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 4125)
+++ 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>

