{"action":"closed","issue":{"url":"https://api.github.com/repos/punkave/apostrophe/issues/295","labels_url":"https://api.github.com/repos/punkave/apostrophe/issues/295/labels{/name}","comments_url":"https://api.github.com/repos/punkave/apostrophe/issues/295/comments","events_url":"https://api.github.com/repos/punkave/apostrophe/issues/295/events","html_url":"https://github.com/punkave/apostrophe/issues/295","id":95518988,"number":295,"title":"CSRF attack protection is needed","user":{"login":"boutell","id":32125,"avatar_url":"https://avatars.githubusercontent.com/u/32125?v=3","gravatar_id":"","url":"https://api.github.com/users/boutell","html_url":"https://github.com/boutell","followers_url":"https://api.github.com/users/boutell/followers","following_url":"https://api.github.com/users/boutell/following{/other_user}","gists_url":"https://api.github.com/users/boutell/gists{/gist_id}","starred_url":"https://api.github.com/users/boutell/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/boutell/subscriptions","organizations_url":"https://api.github.com/users/boutell/orgs","repos_url":"https://api.github.com/users/boutell/repos","events_url":"https://api.github.com/users/boutell/events{/privacy}","received_events_url":"https://api.github.com/users/boutell/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/punkave/apostrophe/labels/0.6+","name":"0.6+","color":"bfe5bf"},{"url":"https://api.github.com/repos/punkave/apostrophe/labels/question","name":"question","color":"f7c6c7"},{"url":"https://api.github.com/repos/punkave/apostrophe/labels/review","name":"review","color":"5319e7"},{"url":"https://api.github.com/repos/punkave/apostrophe/labels/security","name":"security","color":"e11d21"}],"state":"closed","locked":false,"assignee":null,"milestone":null,"comments":6,"created_at":"2015-07-16T20:20:41Z","updated_at":"2016-02-01T15:39:28Z","closed_at":"2016-02-01T15:39:28Z","body":"Right now Apostrophe doesn't specifically protect against CSRF attacks on its APIs. So it is possible to create a form on a third-party site whose action is set to an Apostrophe API, and trick someone who has the right privileges into clicking a submit button on that third-party site to create a page or changing someone's permissions on an Apostrophe site.\r\n\r\nThis is currently unlikely because there are few opportunities to use these tactics against known Apostrophe admins. But it should still be addressed properly and as A2 becomes more common it will be a more significant issue. It also appears prominently in the OWASP Top 10 and other security guidelines.\r\n\r\nOne strategy is to generate a hidden form element that is specific to the current user and won't be accepted from any other user. Directly adding this to every form or JS API call is tedious for front-end developers.\r\n\r\nAnother strategy is to automatically enhance forms and jQuery API calls with such a CSRF code via javascript in the browser. This is an attractive solution because you can't forget to address it. We know it works because we're already enhancing $.get, $.post, $.ajax, etc. in this way for Apostrophe's prefix feature.\r\n\r\n**GET-method links should never be used to carry out actions, period.** That requires putting the CSRF code in the address bar and the hover state of the link, which greatly increases the change it'll be accidentally shared.\r\n\r\nI would like to arrive at an automatic solution if possible.\r\n\r\nSymfony 1.x generated a CSRF token based on the user's session identifier and the name of the form class being submitted, to prevent a CSRF token for one action from being used to carry out another. My beef with this is that it is much more work (it can't be a transparent solution) and if someone has access to one of your tokens, they presumably have access to your session cookie too, and the game is already over.\r\n\r\nMy strong feeling about \"replay attacks\" is that if you don't want people reading your traffic (including your session key), you gotta use HTTPS, in which case replay attacks are not possible.\r\n\r\nSERVER SIDE\r\n\r\n```javascript\r\napos.csrfSecret = options.csrfSecret;\r\n```\r\n\r\nIn renderPage:\r\n\r\n```javascript\r\nself.req.browserCall('apos.setCsrfToken(?)', hash(req.user.session.id + ':' + self.csrfSecret));\r\n```\r\n\r\nIn global middleware:\r\n\r\n```javascript\r\napp.use(function(req, res) {\r\n  if (!req.body) {\r\n    return next();\r\n  }\r\n  if (req.body.csrfToken !== hash(req.user.session.id + ':' + self.csrfSecret)) {\r\n    // Maybe do a 404, why give them information? But technically it's Forbidden\r\n    res.statusCode = 403;\r\n    return res.send('Forbidden');\r\n  }\r\n  return next();\r\n});\r\n```\r\n\r\nIn browserland:\r\n\r\n```javascript\r\napos.setCsrfToken = function(token) {\r\n  apos.csrfToken = token;\r\n}\r\n\r\n$.ajaxPrefilter(function(options, originalOptions, jqXHR) {\r\n  if (options.data && (typeof(options.data) === 'object')) {\r\n    options.data.csrfToken = apos.csrfToken;\r\n  }\r\n});\r\n```\r\n\r\nA disadvantage of this approach is that ALL POST requests must have a CSRF token. Also there must be a consistent session ID assigned for everyone, even an anonymous user. We can't just skip out of the middleware if there is no token, that would completely defeat the purpose and open everything up again.\r\n\r\nWe would also need a way to do this for all vanilla form submissions (non-AJAX). We could do this:\r\n\r\n```javascript\r\n$('body').on('submit', function() {\r\n  var $hidden = $('<input type=\"hidden\" name=\"csrfToken\" />');\r\n  $hidden.val(apos.csrfToken);\r\n  $(this).closest('form').append($hidden);\r\n  return true;\r\n});\r\n```\r\n"}}