DEV Community

Kaj
Kaj

Posted on

Your scripted POST fails with 422 "Invalid authenticity token". It's the token, not your cookies.

I automate the things I shouldn't have to click, and one class of bug kept eating afternoons: a scripted form POST comes back 422 Invalid authenticity token. The cookie is fine. The session is fine. The token is the problem, and it's wrong in two different ways that look identical from the outside.

Two ways a token is dead on arrival

1. The placeholder. Some forms ship a token field whose value is a JS placeholder, overwritten on load. If your script reads raw HTML instead of a live DOM, you grab the placeholder. On dev.to the comment form's authenticity_token is literally the string NOTHING. Post that back and you're sending the word NOTHING as your CSRF token.

2. The wrong session. The token is minted for the session that produced it (in Rails terms, it's derived from _csrf_token in the session cookie). So it's session-bound, not page-bound. Fetch a page with one cookie jar and POST with another, or reuse a token from before you logged in again, and it's dead even though nothing about the string looks expired.

Both fail the same way. Neither is a cookie problem, and logging in again won't help.

The fix

Fetch the page you're about to POST to, in the same session, right before the POST, and take a token from that response:

JAR=cookies.txt
UA="Mozilla/5.0 ..."
PAGE="https://dev.to/<slug>"

curl -s -b $JAR -c $JAR -A "$UA" "$PAGE" -o page.html

# first try a meta csrf-token
TOKEN=$(grep -oE 'name="csrf-token" content="[^"]+"' page.html | head -1 | sed -E 's/.*content="([^"]+)".*/\1/')
# fallback: first authenticity_token that is NOT the placeholder
[ -z "$TOKEN" ] && TOKEN=$(grep -oE 'name="authenticity_token" value="[^"]+"' page.html \
  | grep -v 'NOTHING' | head -1 | sed -E 's/.*value="([^"]+)".*/\1/')

curl -s -b $JAR -c $JAR -A "$UA" -X POST "https://dev.to/comments" \
  -H "X-Requested-With: XMLHttpRequest" \
  -H "Referer: $PAGE" \
  --data-urlencode "authenticity_token=$TOKEN" \
  --data-urlencode "comment[body_markdown]=..." \
  --data-urlencode "comment[commentable_id]=<article id>" \
  --data-urlencode "comment[commentable_type]=Article"
Enter fullscreen mode Exit fullscreen mode

Three rules matter more than the exact regexes:

  • Same cookie jar for GET and POST. -b jar -c jar on both, no exceptions.
  • Skip the placeholder. Take the first token whose value isn't NOTHING (or whatever placeholder your target uses). Know where the token came from before you trust it.
  • The token has to belong to the session you POST with. Fetch it in that session, right before you send. (Some sites do rotate or time-limit tokens anyway, so a token cached across days is a bad bet.)

The general version

A 422 on a scripted form POST is almost never "my cookies are broken." It's "the token I sent wasn't minted for this request." So before you debug auth, prove the token is live: fetch the page in this session, extract a non-placeholder token, send it back in the same cycle. If it still fails, then it's the session.

I paid for this lesson with a week of 422s. If you're automating something with a placeholder token field, tell me what site. I'd bet the same pattern is everywhere.

Top comments (0)