Hank Stoever
part time nerd, part time gnar.

Breaking Hacker News's CSRF Protections

posted almost 4 years ago - 2 min read

CSRF protection is a method websites use to prevent cross-site request forgery. Basically, the goal is to only allow certain endpoints of a website to be accessed through a form originating on their site. An example of an 'attack' this may try to prevent could be brute-force password cracking bots. It basically works by adding a CSRF token to every web form. When the form is submitted, the CSRF token can be validated to ensure it originated from their site.

This approach is almost useless, because the 'attacker' only requires one extra request to obtain a valid token. I'm going to demonstrate how this can be done on hacker news to sign in.

The url to sign in to hacker news is https://news.ycombinator.com/y. If you send a GET request to this url, you receive Unknown or expired link. We'll need use a POST request and figure out what parameters are required. To do this, visit https://news.ycombinator.com/newslogin, right click on the username input and click Inspect Element. Examine the HTML form and find the names of the input tags. These are the parameters we need to use. For logging in, the username field is named u and the password field is named p.

inspect element

hurl.it is a nice website for debugging HTTP requests. You can see this hurl for a place to input your own username and password.

Submitting this still either returns Unknown or expired link or \r\n, even if I use correct login parameters. We'll know it's a success when the response contains a Set-Cookie header. Look back at the form we saw with Inspect Element. There is another input named fnid. This is Hacker New's CSRF token. If you request the login page and copy and paste the fnid field into hurl.it, it's a success! We see that Set-Cookie header we're looking for.

set-cookie

If we save this cookie, we can do thing like post comments and submit to Hacker News. The important thing to remember is that we need to get a fresh CSRF token for every attempt. Here is what the ruby code looks like for this:

def get_login_cookie username, password
  doc = Nokogiri::HTML(RestClient.get("https://news.ycombinator.com/newslogin"))
  fnid = doc.css("input[name='fnid']")[0][:value]
  login_params = {u: username, p: password, fnid: fnid}
  cookie = nil
  RestClient.post('https://news.ycombinator.com/y', login_params){ |response|
    cookie = response.cookies["user"]
  }
  cookie
end

I've created a Ruby gem that implements this logic and even supports posting to Hacker News. I use it for hnbuffer. Here is what the code looks like for submitting new posts:

def get_submit_fnid cookie
  headers = { "Cookie" => "user=#{cookie}" }
  doc = Nokogiri::HTML(RestClient.get("https://news.ycombinator.com/submit", headers))
  fnid = doc.css("input[name='fnid']")[0][:value]
end

def post_to_hn username, password, title, url, body=nil
  cookie = get_login_cookie(username, password)
  fnid = get_submit_fnid(cookie)
  params = {
    fnid: fnid,
    t: title
  }
  headers = {
    "Cookie" => "user=#{cookie}",
    "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Content-Type" => "application/x-www-form-urlencoded",
    "User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31",
    "Origin" => "https://news.ycombinator.com",
    "Host" => "news.ycombinator.com"
  }
  if body.empty?
    params[:u] = url
  else
    params[:x] = body
  end
  res = RestClient.post("https://news.ycombinator.com/r", params, headers){ |response, request, result, &block|
    if [301, 302, 307].include? response.code
      response.follow_redirection(request, result, &block)
    else
      response.return!(request, result, &block)
    end
  }
end

CSRF protection is really not that effective at protecting your site from people creating external APIs. My suggestion is to forget about it and let developers create software using your services more easily.

comments powered by Disqus