[{"data":1,"prerenderedAt":1332},["ShallowReactive",2],{"writing-archive":3},[4,256,1034],{"id":5,"title":6,"body":7,"date":244,"description":245,"excerpt":246,"extension":247,"image":248,"meta":249,"navigation":250,"path":251,"readTime":252,"seo":253,"stem":254,"__hash__":255},"writing\u002Fwriting\u002Fonline-poker-in-a-document.md","Online Poker in a Document",{"type":8,"value":9,"toc":230},"minimark",[10,34,43,50,53,56,61,69,74,83,89,93,96,108,112,115,121,130,134,151,160,163,168,172,175,184,192,201,204,208,211,216,219,223,226],[11,12,13,14,21,22,27,28,33],"p",{},"Over the winter holidays, I found some time to work on improving my poker game, reading a couple ",[15,16,20],"a",{"href":17,"rel":18},"https:\u002F\u002Fwww.amazon.com\u002FHarrington-Expert-Strategy-Limit-Tournaments-ebook\u002Fdp\u002FB002XQ2C6O",[19],"nofollow","great"," ",[15,23,26],{"href":24,"rel":25},"https:\u002F\u002Fwww.amazon.com\u002FTheory-Poker-Professional-Player-Teaches-ebook\u002Fdp\u002FB001QCYJQ2",[19],"books"," and then applying their theories into practice. While there is no substitute for playing in a casino or at someone's house, one of the quickest ways to get into a game is to play online poker. There are hundreds of platforms out there, and after playing on a few I thought it would be a fun learning experience to create my own, but with a twist: what if you could have a doc that lets you play poker? Our goal at ",[15,29,32],{"href":30,"rel":31},"https:\u002F\u002Fosv.im\u002Fcoda",[19],"Coda"," is to precisely to facilitate that: creating docs as powerful as apps (with the help of formulas, not code), so I set out on an attempt. Read on to see the result and go behind the scenes.",[11,35,36,37,42],{},"Without further ado, here's the doc: ",[15,38,41],{"href":39,"rel":40},"https:\u002F\u002Fcoda.io\u002F@osv\u002Fpoker",[19],"coda.io\u002F@osv\u002Fpoker"," (open on a desktop for the best experience). Get a game going, and it will look something like this:",[11,44,45],{},[46,47],"img",{"alt":48,"src":49},"Multiplayer poker gameplay inside Coda","\u002Fimages\u002Fwriting\u002Fonline-poker-in-a-document\u002Fgameplay.webp",[11,51,52],{},"No team of engineers behind it. No long release cycle or lots of code to maintain. And while there are also no pleasant poker table graphics or pretty animations when someone bets, at the end of the day, this whole game is really just a super-document put together by a single hobbyist with some free time over a few days.",[11,54,55],{},"So what did it take? If you've used Coda before, you'll likely be familiar with some of these concepts. But if not, I hope to show that creating apps and games like this is no longer reserved for folks with a computer science degree: anyone who's written a few formulas in Excel can figure it out.",[57,58,60],"h2",{"id":59},"getting-started","Getting started",[11,62,63,64,68],{},"The first step to putting together a doc like this is to think through the ",[65,66,67],"em",{},"schema",", i.e., the different kinds of data or entities that comprise a poker game, which in Coda are collected into tables.",[70,71,73],"h3",{"id":72},"cards","Cards",[11,75,76,77,82],{},"Naturally the first thing my mind jumped to when putting together a poker game was the deck. After finding a set of public domain card images ",[15,78,81],{"href":79,"rel":80},"https:\u002F\u002Fcode.google.com\u002Farchive\u002Fp\u002Fvector-playing-cards",[19],"online",", I added a table of 52 rows, corresponding to each of the cards. The crucial things to know here about each card are its rank and suit (for identifying different hands later on) as well as the location of each card: whether it's in the deck, dealt out on the table, or in a player's hand.",[11,84,85],{},[46,86],{"alt":87,"src":88},"Cards Coda table","\u002Fimages\u002Fwriting\u002Fonline-poker-in-a-document\u002Fcards-table.webp",[70,90,92],{"id":91},"ranks-and-suits","Ranks and Suits",[11,94,95],{},"Ranks in Poker can be numeric (2–10) or face cards (J, Q, K, and A), and I had a hunch that only keeping around the value as shown on the card wasn't going to be that useful, since Coda won't just magically know that a King is better than a Queen. So, I added another table for Ranks, and while I was at it, one for Suits as well. Later on this turned out useful for adding a \"sort value\" for each rank to enable us to sort cards by how strong they are.",[97,98,99,100,99,104],"figure",{},"\n  ",[46,101],{"src":102,"alt":103},"\u002Fimages\u002Fwriting\u002Fonline-poker-in-a-document\u002Franks-suits-table.webp","Ranks and Suits Coda table",[105,106,107],"figcaption",{},"The \"sort value\" of a rank is an alternative representation of it that can be lexicographically sorted within Coda. The weakest card (two) has a sort value of 0. A ten has a sort value of 8, a jack is a 9, queen is A, king is B, and ace is C.",[70,109,111],{"id":110},"players","Players",[11,113,114],{},"The last major set of entities in poker is the set of players in the game, which is also tied to the hand they form from their pocket cards and the community cards on the table. This Coda table started out fairly simple, but actually grew to about 60 columns, containing info such as the user playing as each player, whether it's their turn, whether they're holding a flush or a four-of-a-kind, their role as a dealer or blind in the current round, and so on.",[11,116,117],{},[46,118],{"alt":119,"src":120},"Players Coda table","\u002Fimages\u002Fwriting\u002Fonline-poker-in-a-document\u002Fplayers-table.webp",[11,122,123,124,129],{},"Thanks to Coda's ",[15,125,128],{"href":126,"rel":127},"https:\u002F\u002Fhelp.superhuman.com\u002Fhc\u002Fen-us\u002Farticles\u002F46210184375437-Add-color-and-formatting-to-your-tables",[19],"conditional formats",", it was relatively easy to improve the readability of this table to highlight the current player or someone's turn.",[57,131,133],{"id":132},"game-logic","Game Logic",[11,135,136,137,140,141,144,145,150],{},"With the foundations put in place*, the next step was putting together the game logic that actually allows playing the game, which is made up of two things: ",[65,138,139],{},"state"," and ",[65,142,143],{},"procedures",". Nearly every game needs to keep track of state, and for poker that includes whose turn it is, who's the dealer, the size of the bet, etc. In addition to that, each action that a player takes results in a procedure, or set of steps, happening. An example is that starting a new game should cause the cards to get shuffled. In Coda, this can be accomplished using ",[15,146,149],{"href":147,"rel":148},"https:\u002F\u002Fblog.coda.io\u002Fintroducing-buttons-8acda6413030",[19],"buttons",".",[97,152,99,153,99,157],{},[46,154],{"src":155,"alt":156},"\u002Fimages\u002Fwriting\u002Fonline-poker-in-a-document\u002Fgame-state.webp","State and game engine internals",[105,158,159],{},"A hidden section in the Coda doc that contains the internals of the \"game engine\". There are many more columns that aren't shown here.",[11,161,162],{},"I created a new single-row table called \"State\" to contain all the state and buttons in one place. Feel free to make a copy of the doc and explore all the formulas and buttons to see how they work!",[11,164,167],{"className":165},[166],"footnote","* Actually making this doc involved a lot of back and forth between updating the schema and the game logic, since the latter often depended on updates to the former. For instance, when someone takes their turn, we want to update \"Current Player\" to be the next player to take their turn. This meant adding a new column to the Players table called \"Next Player\", which contains a formula that determines the next, non-folded player in the round.",[57,169,171],{"id":170},"ranking-hands","Ranking Hands",[11,173,174],{},"At the end of a poker hand, the player with the best cards takes the pot (well, without getting into complexities). To determine the winning player, we need to convert each player's hands — which might be a list of the individual cards they're holding — into a sortable representation reflecting the strength of the game. Sorts in Coda are done lexicographically, meaning sequential numbers come before alphabetical letters. For example:",[97,176,99,177,99,181],{},[46,178],{"src":179,"alt":180},"\u002Fimages\u002Fwriting\u002Fonline-poker-in-a-document\u002Ffive-cards-example.webp","Five cards hand representation example",[105,182,183],{},"The five cards above would get represented as \"7–0A\". The \"7\" corresponds to them forming a four-of-a-kind, which is better than a \"6\", which would make a full house. The numbers after the hyphen are used for tie breaking, and in this case refer to the quads having a rank of 2 (the lowest rank, represented as \"0\") with a Q kicker (represented as an \"A\" since it's the 11th best card).",[11,185,186,187,191],{},"If you're interested in the details of how each hand is determined, check out the ",[15,188,190],{"href":39,"rel":189},[19],"\"How It's Made\""," section in the doc. But, here's an example of a formula used to find all the pairs in a player's hand:",[97,193,99,194,99,198],{},[46,195],{"src":196,"alt":197},"\u002Fimages\u002Fwriting\u002Fonline-poker-in-a-document\u002Fformula-example.png","Coda hand ranking formula example",[105,199,200],{},"That \"Ranks\" table we added earlier? I added a new column called \"Player Counts\", which contains the number of times each rank appears in each player's hand. Here we're only returning ranks that occur at least twice in a player's hand.",[11,202,203],{},"With this representation, we can find what the best hand on the table is (by sorting all the representations and taking the last one), and subsequently the player(s) who have it.",[57,205,207],{"id":206},"putting-it-all-together","Putting it all together",[11,209,210],{},"After a lot of trial and error putting together the schema and game logic to get the intricacies of poker working (e.g., raising, side pots, ties), the final step was creating a user interface on top of all the backend tables and game state. In Coda this is done using views, which provide an alternative way to view the same table.",[11,212,213],{},[46,214],{"alt":207,"src":215},"\u002Fimages\u002Fwriting\u002Fonline-poker-in-a-document\u002Fputting-it-all-together.webp",[11,217,218],{},"In the screenshot above, the two blocks in #1 are actually views of the Cards table, showing only the \"Card Image\" column and filtered down to cards held by the current player and in the community, respectively. Block #2 contains a formula summarizing the game state in a user-friendly way (i.e., showing whose turn it is or if someone won a given pot). And finally, block #3 is a view on the players table, hiding most of the 60 columns associated with each player and only showing the player number, their role, the user behind them, their balance, revealed cards at the end of a hand, and how much they've bet in the current round.",[57,220,222],{"id":221},"reflections","Reflections",[11,224,225],{},"Building a poker game as a doc in Coda was rewarding, but ultimately it wouldn't be possible without the progress made as part of the nascent no-code movement. New platforms (like Coda) are allowing makers with domain-specific knowledge and a curious mindset the ability to build solutions to complex problems without requiring engineering knowledge. These tools aren't yet fitting for every problem — for instance, you won't be able to make a distributed search engine or a self-driving car — but that's ok. There are plenty of use cases, such as platforms for small communities or business tools, that can be vastly made easier to create, and the future is looking brighter for helping us spend less time and resources on developing these tools and more on the problems we're trying to solve with them.",[11,227,229],{"className":228},[166],"Disclosure: links to Coda here include a referral link, which credits your new account with $10 (at the time of writing). As an employee, I don't personally get anything out of referrals, but having worked on our referral program I wanted to help out anyone who signs up after reading this article. :)",{"title":231,"searchDepth":232,"depth":232,"links":233},"",2,[234,240,241,242,243],{"id":59,"depth":232,"text":60,"children":235},[236,238,239],{"id":72,"depth":237,"text":73},3,{"id":91,"depth":237,"text":92},{"id":110,"depth":237,"text":111},{"id":132,"depth":232,"text":133},{"id":170,"depth":232,"text":171},{"id":206,"depth":232,"text":207},{"id":221,"depth":232,"text":222},"2020-02-29","Over the winter holidays, I found some time to work on improving my poker game, reading a couple great books and then applying their theories into practice. While there is no substitute for playing in a casino or at someone's house, one of the quickest ways to get into a game is to play online poker. There are hundreds of platforms out there, and after playing on a few I thought it would be a fun learning experience to create my own, but with a twist: what if you could have a doc that lets you play poker? Our goal at Coda is to precisely to facilitate that: creating docs as powerful as apps (with the help of formulas, not code), so I set out on an attempt. Read on to see the result and go behind the scenes.",null,"md","\u002Fimages\u002Fwriting\u002Fonline-poker-in-a-document\u002Fcover.png",{},true,"\u002Fwriting\u002Fonline-poker-in-a-document",6,{"title":6,"description":245},"writing\u002Fonline-poker-in-a-document","Gkw5qTOfOp-XcYmioRGIBhNm-4aQ-3RF6-SaR7EX_p8",{"id":257,"title":258,"body":259,"date":1026,"description":1027,"excerpt":246,"extension":247,"image":284,"meta":1028,"navigation":250,"path":1029,"readTime":1030,"seo":1031,"stem":1032,"__hash__":1033},"writing\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat.md","Google CTF Competition 2018: Cat Chat",{"type":8,"value":260,"toc":1010},[261,276,279,285,288,292,304,313,322,331,337,340,357,366,374,382,386,396,402,419,427,434,441,447,450,453,459,466,510,519,523,526,529,535,538,542,545,551,554,590,594,607,611,621,628,637,643,647,650,653,667,691,723,729,757,761,768,774,780,783,838,842,861,867,870,876,879,881,884,899,902,906,913,919,922,931,935,944,1003,1006],[11,262,263,264,269,270,275],{},"This past weekend Google held the qualification round for its third annual ",[15,265,268],{"href":266,"rel":267},"https:\u002F\u002Fsecurity.googleblog.com\u002F2018\u002F05\u002Fgoogle-ctf-2018-is-here.html",[19],"Capture the Flag competition",", with 25 demanding challenges in categories ranging from cryptography to web to binary exploits. While my skillset goes nowhere near close to that of the ",[15,271,274],{"href":272,"rel":273},"https:\u002F\u002Fctftime.org\u002Fevent\u002F623",[19],"talented teams"," participating from all over the world, I had some time to try my hand at one of the web challenges, Cat Chat, and wanted to document my approach. In my opinion, CTFs can be a great learning experience and taking a stab at some challenges as well as reading people's write-ups after can be great for becoming a better engineer.",[11,277,278],{},"This challenge wasn't trivial, but in the end I was able to get the flag:",[11,280,281],{},[46,282],{"alt":283,"src":284},"Google CTF flag display","\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fflag-unlocked.webp",[11,286,287],{},"Read on to find out how!",[57,289,291],{"id":290},"starting-out","Starting Out",[11,293,294,297,298,303],{},[65,295,296],{},"Cat Chat"," reminded me of IRC hacking challenges popular a decade ago, like the ones on ",[15,299,302],{"href":300,"rel":301},"https:\u002F\u002Fwww.hackthissite.org\u002F",[19],"HackThisSite",". I'll let the screenshot speak for itself, but the premise is that there's a chat app run by someone bent against canines, and the goal is to (surprise!) steal the admin's credentials to get the flag.",[97,305,99,306,99,310],{},[46,307],{"src":308,"alt":309},"\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fcat-chat-ui.webp","Cat Chat UI",[105,311,312],{},"Sprinkle on a sidebar and some emojis, and you'll have Slack. :)",[11,314,315,316,321],{},"Reading over the preface, it looks like we get access to the ",[15,317,320],{"href":318,"rel":319},"https:\u002F\u002Fexpressjs.com\u002F",[19],"Express"," server's source code! And naturally we have access to the client's source as well. In general with these types of challenges, an effective approach is to first explore the app itself, and then do a code review and spot any weaknesses that may have been (usually intentionally) introduced. [I'll denote those with a 🚩.]",[97,323,99,324,99,328],{},[46,325],{"src":326,"alt":327},"\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fchange-name-prompt.webp","Change name prompt",[105,329,330],{},"The script kiddy's favorite name, age, and location.",[11,332,333],{},[46,334],{"alt":335,"src":336},"Script injection attempt","\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fscript-injection-failed.webp",[11,338,339],{},"To start things off, we can change our name 🚩 to anything, so why not do the quick-n-dirty inline script test? Unfortunately, no dice — the input is escaped.",[11,341,342,343,346,347,352,353,356],{},"Moving on, the admin ",[65,344,345],{},"really",", ",[65,348,349],{},[350,351,345],"strong",{}," hates dogs. As a test, let's see what happens when we join the same room as another user (i.e., in an incognito window) and start talking about dogs. The admin comes in, bans anyone who utters the word (ahem, ",[350,354,355],{},"red_bombay","), and promptly disconnects. That's interesting — we can summon the admin to our chatroom at any time. Once we figure out what exactly makes the admin an admin, maybe we can somehow compromise them?",[11,358,359,360,365],{},"Before we go on, let's also look at one more thing: whether the client sets a ",[15,361,364],{"href":362,"rel":363},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FCSP",[19],"Content Security Policy",". If it does (as any modern web app should), that will significantly decrease our client-side attack vectors.",[97,367,99,368,99,372],{},[46,369],{"src":370,"alt":371},"\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fcsp-headers-network.webp","Network tab in Chrome's Developer Tools",[105,373,371],{},[11,375,376,377,381],{},"Sure enough, the CSP here is pretty strict. Even if we find an opportunity to inject some JavaScript into the page, the CSP will prevent the browser from executing it. The only thing that's of interest is the ",[378,379,380],"code",{},"'unsafe-inline'"," CSS policy 🚩, but how often can a stylesheet be used in an attack? 🤨",[57,383,385],{"id":384},"inspecting-the-client","Inspecting the client",[11,387,388,389,140,392,395],{},"Looking at the main page's source code, we get a huge hint about two secret commands 🚩, ",[378,390,391],{},"\u002Fsecret",[378,393,394],{},"\u002Fban",":",[11,397,398],{},[46,399],{"alt":400,"src":401},"Client secret commands","\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fclient-secret-commands.webp",[11,403,404,405,408,409,411,412,414,415,418],{},"We now know ",[65,406,407],{},"how"," the admin actually bans unwelcome 🐕 supporters, and more importantly we know ",[65,410,407],{}," to become the admin. Running the ",[378,413,391],{}," command authenticates the user; we'll later discover that using it sets a cookie named ",[378,416,417],{},"flag"," in the browser 🚩:",[97,420,99,421,99,425],{},[46,422],{"src":423,"alt":424},"\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fchrome-application-cookies.webp","Chrome's Application tab in Developer Tools",[105,426,424],{},[11,428,429,430,433],{},"So, to get the flag ",[350,431,432],{},"we have to steal the admin's secret cookie",". Easier said than done!",[11,435,436,437,440],{},"Let's proceed further and look at the client, ",[378,438,439],{},"catchat.js",". First thing that we notice is the admin is quite lazy, running a function to look for the word \"dog\" and ban anyone who said it.",[11,442,443],{},[46,444],{"alt":445,"src":446},"Client admin ban logic","\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fclient-admin-ban-logic.webp",[11,448,449],{},"We know that we can change our name to anything, so let's keep that in mind — the admin will repeat any name that we give it 🚩.",[11,451,452],{},"Proceeding further, we can see how all the different types of messages are handled:",[11,454,455],{},[46,456],{"alt":457,"src":458},"Client message handlers","\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fclient-message-handlers.webp",[11,460,461,462,465],{},"Rats — the code escapes all untrusted user input with an ",[378,463,464],{},"esc()"," function, so we can't just start writing our own HTML. But there are still a few things of interest here:",[467,468,469,484,491],"ol",{},[470,471,472,473,475,476,479,480,483],"li",{},"Whenever someone runs the ",[378,474,391],{}," command, the password gets stashed under a ",[378,477,478],{},"\u003Cspan>"," into its ",[378,481,482],{},"data-secret"," attribute. So in addition to living in the cookie, the secret also gets written into the page. 🚩",[470,485,486,487,490],{},"Banning is strictly a client-side thing — the server just sets a ",[378,488,489],{},"banned"," cookie that we can easily clear out. 🚩",[470,492,493,494,497,498,501,502,505,506,509],{},"Remember how a banned user becomes red in the chat room? Apparently that's done using some CSS that matches all divs whose ",[378,495,496],{},"data-name"," attribute ",[65,499,500],{},"starts with"," the banned user's name 🚩. Looks like the developer forgot to quote the name though, and there's no good reason to use ",[378,503,504],{},"^="," instead of ",[378,507,508],{},"="," for the predicate. ¯\\_(ツ)_\u002F¯",[11,511,512,513,518],{},"Other than that, the code looks reasonably robust. The rest of it deals with handling reporting user a ",[15,514,517],{"href":515,"rel":516},"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fintro\u002Fv3beta.html",[19],"reCAPTCHA"," to prevent spamming the admin and generating a new name upon connection. We've gleaned enough from here, so let's hop over and look at the server's source code.",[57,520,522],{"id":521},"inspecting-the-server","Inspecting the server",[11,524,525],{},"Looking through the server's source resulted in a number of interesting observations.",[70,527,364],{"id":528},"content-security-policy",[11,530,531],{},[46,532],{"alt":533,"src":534},"Server CSP definition","\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fserver-csp-definition.webp",[11,536,537],{},"As we saw earlier, the server sets a CSP to help prevent cross-site scripting and the like.",[70,539,541],{"id":540},"incoming-messages-handler","Incoming Messages Handler",[11,543,544],{},"This handler is the meat of the chat server; it handles all incoming messages:",[11,546,547],{},[46,548],{"alt":549,"src":550},"Server message router","\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fserver-message-router.webp",[11,552,553],{},"Already, a number of interesting things here:",[467,555,556,559,562,587],{},[470,557,558],{},"In general it's best practice to avoid using GETs for operations that result in change of state. But bizarrely enough, the application accepts messages for all HTTP verbs, including GET. That means that if we have someone simply visit a URL (or perhaps embed an image with that URL as the source), we can potentially get them to send a message. 🚩",[470,560,561],{},"The developer was at least somewhat prudent in implementing a basic CSRF protection mechanism. Without it, we could potentially stage a spear-phishing attack by sending the admin an interesting 🐈 article with a hidden surprise, but looks like that won't work because the referrer will be different than the host. 🤔",[470,563,564,565,568,569,572,573,576,577,580,581,583,584,586],{},"Ah, looks like the dev fell into the common mistake of forgetting a ",[378,566,567],{},"break"," in a ",[378,570,571],{},"switch-case"," statement! 🚩 The starting command is extracted correctly, but each command thereafter essentially only tests for the presence of the command word. Perhaps this was meant to be intentional, but that means typing something like ",[378,574,575],{},"\u002Fname \u002Fban red_bombay"," would change the name to ",[378,578,579],{},"\u002Fban red_bombay"," and then issue a ban command to ban ",[378,582,355],{},". Since we control the text after ",[378,585,394],{}," by changing our name to whatever, we can make the admin change their secret (locking them out of banning) and then have them report the room again.",[470,588,589],{},"Setting the secret apparently just writes that password into the header directly, without URI-encoding it. 🚩 We probably can't use it to set another header, but this might still come in useful...",[57,591,593],{"id":592},"orchestrating-the-attack","Orchestrating the attack",[11,595,596,597,600,601,603,604,606],{},"Our goal is to get the flag from the admin's cookie, and since the CSP prevents any reasonable attempt at writing JavaScript to steal ",[378,598,599],{},"document.cookie",", the only way we can steal it is via that ",[378,602,482],{}," attribute set when running the ",[378,605,391],{}," command. This password is only set once a year, so most likely that element doesn't even exist... but maybe we can trick the admin into running the command?",[70,608,610],{"id":609},"populating-the-secret","Populating the secret",[11,612,613,614,617,618,620],{},"Presumably if we change the name of our dog-loving user to something like ",[378,615,616],{},"\u002Fsecret \u003Cnew secret>",", we can get the admin to execute that command and fill in the ",[378,619,482],{}," attribute with that new secret. But wait! Doing so would erase the flag, right?",[11,622,623,624,627],{},"As it turns out, the cookie header's value is unescaped, meaning we can actually make the header invalid and thus prevent the browser from applying it. There are a number of ways of doing this, but perhaps the most straightforward is to simply set the domain value to something other than ",[378,625,626],{},"cat-chat.web.ctfcompetition.com",". For instance, writing:",[629,630,635],"pre",{"className":631,"code":633,"language":634},[632],"language-text","\u002Fname \u002Fsecret foo; domain=xyz.com\n","text",[378,636,633],{"__ignoreMap":231},[11,638,639,640,642],{},"would end up triggering the logic that populates ",[378,641,482],{}," with the cookie without actually overriding it. We're getting close!",[70,644,646],{"id":645},"extracting-the-secret","Extracting the secret",[11,648,649],{},"We now have a way of tricking the admin into filling the local DOM with the cookie we want to steal, but now we actually have to, well, steal it. We can't inject any scripts, but let's go back and see if there are any useful 🚩.",[11,651,652],{},"That 'inline-style' CSP policy looked suspicious, as did this piece of code that makes the banned user's name red:",[629,654,658],{"className":655,"code":656,"language":657,"meta":231,"style":231},"language-javascript shiki shiki-themes github-light github-dark","display(`${esc(data.name)} was banned.\u003Cstyle>span[data-name^=${esc(data.name)}] { color: red; }\u003C\u002Fstyle>`);\n","javascript",[378,659,660],{"__ignoreMap":231},[661,662,665],"span",{"class":663,"line":664},"line",1,[661,666,656],{},[11,668,669,670,346,673,346,676,679,680,683,684,687,688,690],{},"The name is escaped, meaning we can't use ",[378,671,672],{},"\u003C",[378,674,675],{},">",[378,677,678],{},"'",", or ",[378,681,682],{},"\""," (the dev forgot ",[378,685,686],{},"&","). But we can still close that rule and write any CSS that we want on the page. This may seem far-fetched, but would we be able to steal the contents of ",[378,689,482],{}," with a stylesheet? Putting 2 and 2 together, the idea is to trick the admin like so:",[692,693,694,704,712,720],"ul",{},[470,695,696,697,699,700,703],{},"If the ",[378,698,482],{}," content starts with ",[378,701,702],{},"A",", set the background-image to a logged URL containing \"A\"",[470,705,696,706,699,708,711],{},[378,707,482],{},[378,709,710],{},"B",", set the background-image to a logged URL containing \"B\"",[470,713,696,714,699,716,719],{},[378,715,482],{},[378,717,718],{},"C",", set the background-image to a logged URL containing \"C\"",[470,721,722],{},"... and so on",[11,724,725,726,728],{},"Then, once we get the first letter (let's say it's ",[378,727,718],{},"):",[692,730,731,739,747,755],{},[470,732,696,733,699,735,738],{},[378,734,482],{},[378,736,737],{},"CA",", set the background-image to a logged URL containing \"CA\"",[470,740,696,741,699,743,746],{},[378,742,482],{},[378,744,745],{},"CB",", set the background-image to a logged URL containing \"CB\"",[470,748,696,749,699,751,754],{},[378,750,482],{},[378,752,753],{},"CC",", set the background-image to a logged URL containing \"CC\"",[470,756,722],{},[70,758,760],{"id":759},"bypassing-the-csp","Bypassing the CSP",[11,762,763,764,767],{},"The next step is to actually log these requests, so that we know when we've guessed a letter correctly. Let's just spin up a quick Droplet and run ",[378,765,766],{},"python -m SimpleHTTPServer 80",", right?",[11,769,770,773],{},[65,771,772],{},"Not so fast!"," The server's CSP actually prevents embedding any external images. But, recall that we can actually send messages to any chatroom via a simple URL. So we could instead use an image source like:",[629,775,778],{"className":776,"code":777,"language":634},[632],"https:\u002F\u002Fcat-chat.web.ctfcompetition.com\u002Froom\u002F\u003Croom-id>\u002Fsend?name=name&msg=message\n",[378,779,777],{"__ignoreMap":231},[11,781,782],{},"To put this into context, the CSS rule would look like this:",[629,784,788],{"className":785,"code":786,"language":787,"meta":231,"style":231},"language-css shiki shiki-themes github-light github-dark","span[data-secret^=A] {\n  background: url(https:\u002F\u002Fcat-chat.web.ctfcompetition.com\u002Froom\u002F\u003Croom-id>\u002Fsend?name=the%20password%20is&msg=A)\n}\n","css",[378,789,790,811,833],{"__ignoreMap":231},[661,791,792,795,799,802,805,808],{"class":663,"line":664},[661,793,661],{"class":794},"s9eBZ",[661,796,798],{"class":797},"sVt8B","[",[661,800,482],{"class":801},"sScJk",[661,803,504],{"class":804},"szBVR",[661,806,702],{"class":807},"sZZnC",[661,809,810],{"class":797},"] {\n",[661,812,813,817,820,823,826,830],{"class":663,"line":232},[661,814,816],{"class":815},"sj4cs","  background",[661,818,819],{"class":797},": ",[661,821,822],{"class":815},"url",[661,824,825],{"class":797},"(",[661,827,829],{"class":828},"s4XuR","https:\u002F\u002Fcat-chat.web.ctfcompetition.com\u002Froom\u002F\u003Croom-id>\u002Fsend?name=the%20password%20is&msg=A",[661,831,832],{"class":797},")\n",[661,834,835],{"class":663,"line":237},[661,836,837],{"class":797},"}\n",[70,839,841],{"id":840},"proof-of-concept","Proof-of-concept",[11,843,844,845,140,848,851,852,857,858,150],{},"Provided that the character set of the flag is restricted (we'll assume uppercase letters, numbers, and ",[378,846,847],{},"{",[378,849,850],{},"}"," for now), let's try this out on ourselves first. We can't try too many characters at once ",[15,853,856],{"href":854,"rel":855},"https:\u002F\u002Ftools.ietf.org\u002Fhtml\u002Frfc7230#section-3.1.1",[19],"because URLs can only be so big",". Note that to make the rest of the CSS valid we can simply end with a ",[378,859,860],{},"\u002F*",[11,862,863],{},[46,864],{"alt":865,"src":866},"CSS injection exploit","\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fcss-injection-exploit.webp",[11,868,869],{},"That worked! Here's what happened:",[11,871,872],{},[46,873],{"alt":874,"src":875},"Secret exfiltration","\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fsecret-exfiltration.webp",[11,877,878],{},"We successfully injected some CSS to trick the browser into issuing a request to post a message to the chat.",[70,880,207],{"id":206},[11,882,883],{},"We've now identified a number of useful deficiencies in the code to put together a successful cookie-stealing attack. To make it all work, we'll want to trick the admin into running a command that will:",[467,885,886,896],{},[470,887,888,889,892,893,895],{},"Set their ",[350,890,891],{},"secret"," to something that won't actually override it but still set the ",[378,894,482],{}," attribute in the page. ✅",[470,897,898],{},"Inject some stylesheet rules that will log a request to a chat room based on the first (or nth) letter of the secret. ✅",[11,900,901],{},"Now, writing this payload by hand is laborious, so let's write a snippet to do it for us:",[903,904],"gist-embed",{"gist-id":905},"8547d242520567738e5ef8a0b9f9bb26",[11,907,908,909,912],{},"Let's start out by opening two chat sessions in the same room, changing the name of one of the users, issuing a ",[378,910,911],{},"\u002Freport",", and saying some poochy profanity. Success!",[11,914,915],{},[46,916],{"alt":917,"src":918},"CSRF exploit delivery","\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fcsrf-exploit-delivery.png",[11,920,921],{},"Rinse and repeat (clearing the banned cookie and refreshing each time in the banned user's session), and we get our flag:",[97,923,99,924,99,928],{},[46,925],{"src":926,"alt":927},"\u002Fimages\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat\u002Fpayload-generator-draft.png","Payload generator draft",[105,929,930],{},"Before I tidied up the payload generator. :)",[57,932,934],{"id":933},"learnings","Learnings",[11,936,937,938,943],{},"While clearly staged, this was a fun challenge and I enjoyed putting together the fabled character-by-character CSS attack (you may have seen ",[15,939,942],{"href":940,"rel":941},"https:\u002F\u002Fnews.ycombinator.com\u002Fitem?id=16422696",[19],"this CSS keylogger Hacker News post"," a few months ago). But there are some lessons to take away here:",[692,945,946,957,965,974,994,997,1000],{},[470,947,948,949,952,953,956],{},"Set a ",[15,950,364],{"href":362,"rel":951},[19]," on your site to offer a last-resort protection against malicious user-injected content, and be extra careful about allowing access to sources you don't control, especially ",[378,954,955],{},"unsafe-inline",". For inline scripts and styles, use a nonce. (This mechanism will become more secure as additional browsers support CSP v3.)",[470,958,959,960,140,962,964],{},"Don't leak the mechanisms of your admin functionality. While relying on security through obscurity is never good, this attack would have been significantly harder to execute without knowing the ",[378,961,391],{},[378,963,394],{}," commands; a separate admin client would have been a good deterrent.",[470,966,967,968,973],{},"Follow the ",[15,969,972],{"href":970,"rel":971},"https:\u002F\u002Fwww.w3.org\u002F2001\u002Ftag\u002Fdoc\u002FwhenToUseGet.html#checklist",[19],"REST spec for defining endpoints"," — GET requests should be idempotent and never mutate any resources.",[470,975,976,977,985,986,988,989,150],{},"Similar to ",[15,978,981,982],{"href":979,"rel":980},"https:\u002F\u002Fxkcd.com\u002F292\u002F",[19],"avoiding ",[378,983,984],{},"goto",", be extra careful about complex ",[378,987,571],{}," statements, particularly jumping across cases (and if you have to, leave clear comments). Doing otherwise can ",[15,990,993],{"href":991,"rel":992},"https:\u002F\u002Fwww.synopsys.com\u002Fblogs\u002Fsoftware-security\u002Fgimme-a-break\u002F",[19],"set you up for failure",[470,995,996],{},"If you have to deal with banning users, don't rely on cookies; there are plenty of better alternatives (reCAPTCHAs, user accounts, Cloudflare, etc.).",[470,998,999],{},"Escape user input, always use quotes around attributes, and URI-encode cookie header contents (or use a library).",[470,1001,1002],{},"Don't hate on dogs. :)",[11,1004,1005],{},"If you made it this far, I hope you enjoyed reading this and took something out of it! And if I missed something, please let me know. Happy hacking!",[1007,1008,1009],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":231,"searchDepth":232,"depth":232,"links":1011},[1012,1013,1014,1018,1025],{"id":290,"depth":232,"text":291},{"id":384,"depth":232,"text":385},{"id":521,"depth":232,"text":522,"children":1015},[1016,1017],{"id":528,"depth":237,"text":364},{"id":540,"depth":237,"text":541},{"id":592,"depth":232,"text":593,"children":1019},[1020,1021,1022,1023,1024],{"id":609,"depth":237,"text":610},{"id":645,"depth":237,"text":646},{"id":759,"depth":237,"text":760},{"id":840,"depth":237,"text":841},{"id":206,"depth":237,"text":207},{"id":933,"depth":232,"text":934},"2018-06-25","This past weekend Google held the qualification round for its third annual Capture the Flag competition, with 25 demanding challenges in categories ranging from cryptography to web to binary exploits. While my skillset goes nowhere near close to that of the talented teams participating from all over the world, I had some time to try my hand at one of the web challenges, Cat Chat, and wanted to document my approach. In my opinion, CTFs can be a great learning experience and taking a stab at some challenges as well as reading people's write-ups after can be great for becoming a better engineer.",{},"\u002Fwriting\u002Fgoogle-ctf-competition-2018-cat-chat",10,{"title":258,"description":1027},"writing\u002Fgoogle-ctf-competition-2018-cat-chat","Pz1_-l7_-jituwXC3F9rD_S34LuOFZ37nEyAW-5XRZM",{"id":1035,"title":1036,"body":1037,"date":1324,"description":1325,"excerpt":246,"extension":247,"image":1303,"meta":1326,"navigation":250,"path":1327,"readTime":1328,"seo":1329,"stem":1330,"__hash__":1331},"writing\u002Fwriting\u002Fthe-coda-bell.md","The Coda Bell",{"type":8,"value":1038,"toc":1319},[1039,1047,1051,1060,1065,1074,1089,1093,1118,1124,1127,1133,1137,1144,1166,1183,1187,1190,1199,1217,1221,1224,1232,1236,1239,1243,1246,1250,1259,1262,1275,1288,1292,1299,1307,1316],[11,1040,1041,1042,1046],{},"A few weeks ago, I got to be part of something special: the launch of ",[15,1043,32],{"href":1044,"rel":1045},"https:\u002F\u002Fblog.coda.io\u002Fits-a-new-day-for-docs-2643fb16f05a",[19],". To add a little ceremony to the occasion (it’s not every day you get to launch a company!), my coworker Chris Eck and I rigged up an old call bell to ring every time someone signed up. Here’s what it looked like:",[1048,1049],"tweet",{"id":1050},"922964153605558272",[11,1052,1053,1054,1059],{},"One of our product managers, Matt Hudson, actually deserves credit for the whole idea. He had a ",[15,1055,1058],{"href":1056,"rel":1057},"https:\u002F\u002Fwww.amazon.com\u002Fgp\u002Fproduct\u002FB001B095E0\u002F",[19],"call bell"," lying around, so we just needed to pick up a few extra parts and go from there. This post goes into how we built the Coda Bell.",[1061,1062,1064],"h1",{"id":1063},"parts-and-equipment","Parts and Equipment",[11,1066,1067,1068,1073],{},"The most important part of a project like this is the brains — the microcontroller. I’ve personally used a bunch, ranging from a Raspberry Pi (one of the smallest Linux boxes out there) to an ESP8266 (awesome cheap WiFi chip!), but for the purpose of this project, we decided to go with a ",[15,1069,1072],{"href":1070,"rel":1071},"https:\u002F\u002Fwww.particle.io\u002Fproducts\u002Fhardware\u002Fphoton-wifi-dev-kit",[19],"Particle Photon",", for its excellent out-of-the-box cloud integration (we didn’t have a lot of time to mess with the TCP stack).",[11,1075,1076,1077,1082,1083,1088],{},"There wasn’t too much hardware complexity in the remaining parts: we just used an ",[15,1078,1081],{"href":1079,"rel":1080},"http:\u002F\u002Fakizukidenshi.com\u002Fdownload\u002Fds\u002Ftowerpro\u002FSG90.pdf",[19],"SG90 servo",", an ",[15,1084,1087],{"href":1085,"rel":1086},"https:\u002F\u002Fwww.amazon.com\u002Fgp\u002Fproduct\u002FB0734ZLSQG",[19],"enclosure"," off Amazon, some wires and spare electronics parts, and a breadboard. No soldering necessary!",[1061,1090,1092],{"id":1091},"from-cloud-to-device","From Cloud to Device",[11,1094,1095,1096,1101,1102,1107,1108,1113,1114,1117],{},"When we noticed that Particle ",[15,1097,1100],{"href":1098,"rel":1099},"https:\u002F\u002Fdocs.particle.io\u002Fguide\u002Ftools-and-features\u002Fifttt\u002F",[19],"supported IFTTT"," (IF This Then That), we knew there was no chance we’d bother with polling our servers or dealing with interrupts for something like this. Instead, we went on IFTTT and configured the ",[15,1103,1106],{"href":1104,"rel":1105},"https:\u002F\u002Fifttt.com\u002Fmaker_webhooks",[19],"Webhooks service",", linked our ",[15,1109,1112],{"href":1110,"rel":1111},"https:\u002F\u002Fifttt.com\u002Fparticle",[19],"Particle account",", and created a simple applet to publish a private ",[65,1115,1116],{},"signup"," event to the Particle Cloud every time the webhook URL was hit, which we’d set up to happen when someone signed up.",[11,1119,1120],{},[46,1121],{"alt":1122,"src":1123},"IFTTT","\u002Fimages\u002Fwriting\u002Fthe-coda-bell\u002Fifttt.png",[11,1125,1126],{},"The Webhooks service supports passing in a few values, so we set the data of the Particle event to:",[629,1128,1131],{"className":1129,"code":1130,"language":634},[632],"{{OccurredAt}}  \n{{Value1}}  \n{{Value2}}  \n{{Value3}}\n",[378,1132,1130],{"__ignoreMap":231},[57,1134,1136],{"id":1135},"testing-the-ifttt-applet","Testing the IFTTT Applet",[11,1138,1139,1140,1143],{},"Now, to test that it worked, we used the ",[65,1141,1142],{},"particle-cli"," Node package to listen to the event:",[629,1145,1149],{"className":1146,"code":1147,"language":1148,"meta":231,"style":231},"language-sh shiki shiki-themes github-light github-dark","$ particle subscribe signup mine  \nSubscribing to \"signup\" from my personal stream (my devices only)  \nListening to: \u002Fv1\u002Fdevices\u002Fevents\u002Fsignup\n","sh",[378,1150,1151,1156,1161],{"__ignoreMap":231},[661,1152,1153],{"class":663,"line":664},[661,1154,1155],{},"$ particle subscribe signup mine  \n",[661,1157,1158],{"class":663,"line":232},[661,1159,1160],{},"Subscribing to \"signup\" from my personal stream (my devices only)  \n",[661,1162,1163],{"class":663,"line":237},[661,1164,1165],{},"Listening to: \u002Fv1\u002Fdevices\u002Fevents\u002Fsignup\n",[11,1167,1168,1169,1172,1173,1178,1179,1182],{},"We then got the webhook URL from the Webhook service settings in IFTTT (e.g., ",[378,1170,1171],{},"https:\u002F\u002Fmaker.ifttt.com\u002Ftrigger\u002Fsignup\u002Fwith\u002Fkey\u002Ffoo","), and fired up ",[15,1174,1177],{"href":1175,"rel":1176},"https:\u002F\u002Fchrome.google.com\u002Fwebstore\u002Fdetail\u002Fpostman\u002Ffhbjgbiflinjbdggehcddcbncdddomop?hl=en",[19],"Postman"," to POST to that URL with ",[65,1180,1181],{},"{“value1”: “123”}",". A few seconds later, the message showed up.",[1061,1184,1186],{"id":1185},"putting-the-circuit-together","Putting the Circuit Together",[11,1188,1189],{},"The whole project ended up being relatively straightforward with these components.",[97,1191,99,1192,99,1196],{},[46,1193],{"src":1194,"alt":1195},"\u002Fimages\u002Fwriting\u002Fthe-coda-bell\u002Fschematic.png","Schematic for bell",[105,1197,1198],{},"Coda Bell schematic. Components connected by a dotted line are in practice optional.",[11,1200,1201,1202,1205,1206,1209,1210,140,1213,1216],{},"When powering the Photon via USB, pin 1 supplies 4.8 V, which is in line with the operating voltage of the servo. The bypass capacitor ",[65,1203,1204],{},"C1"," and bleeder resistor ",[65,1207,1208],{},"R2"," in parallel with the servo act as a reservoir of energy for the power-hungry motor, which may otherwise behave erratically and draw too much current, while the ",[65,1211,1212],{},"D1",[65,1214,1215],{},"R1"," at the bottom are there for blinking a status LED.",[1061,1218,1220],{"id":1219},"making-the-bell-ring","Making the Bell Ring",[11,1222,1223],{},"The servo came with a few attachments, which we jerry-rigged to create a lever that swings to knock the clapper of the bell into the metal bowl. We then used epoxy to secure the servo in place.",[97,1225,99,1226,99,1229],{},[46,1227],{"src":1228,"alt":1195},"\u002Fimages\u002Fwriting\u002Fthe-coda-bell\u002Fbell_underside.jpg",[105,1230,1231],{},"Underside of the bell, showing the mechanics of the servo ringing it.",[1061,1233,1235],{"id":1234},"writing-the-firmware","Writing the Firmware",[11,1237,1238],{},"With the hardware out of the way, it was time to actually hook this thing up! Here’s the code:",[903,1240],{"gist-id":1241,"file":1242},"763dd937f29c3ccc6bfdb3ed6a768b67","coda-bell.c",[11,1244,1245],{},"This was mostly straightforward, but there are a couple things worth pointing out:",[57,1247,1249],{"id":1248},"servo-library","Servo Library",[11,1251,1252,1253,1258],{},"Particle ships with a ",[15,1254,1257],{"href":1255,"rel":1256},"https:\u002F\u002Fdocs.particle.io\u002Freference\u002Ffirmware\u002Fphoton\u002F#servo",[19],"servo library",", which handled sending a pulse-width modulation (PWM) signal to the motor for us. We just tell it what position (in degrees) to go to, delay as needed, and it does the rest.",[57,1260,1122],{"id":1261},"ifttt",[11,1263,1264,1265,1270,1271,1274],{},"Thanks to ",[15,1266,1269],{"href":1267,"rel":1268},"https:\u002F\u002Fdocs.particle.io\u002Freference\u002Ffirmware\u002Fphoton\u002F#particle-subscribe-",[19],"Particle Cloud",", listening to the webhook was as easy as calling ",[65,1272,1273],{},"Particle.subscribe(“signup”, onIfttt, MY_DEVICES)",". Our handler here assumes that the second line of the message contains an integer with the number of signups that have occurred since the last event, and the main loop handles consuming this count and ringing that number of times.",[11,1276,1277,1278,1283,1284,1287],{},"Since user signups can be viewed as a ",[15,1279,1282],{"href":1280,"rel":1281},"http:\u002F\u002Fwww.rle.mit.edu\u002Frgallager\u002Fdocuments\u002F6.262lateweb2.pdf",[19],"Poisson process",", we wanted to make sure that the bell is able to cope with sudden bursts of people signing up, and this approach established a queue of sorts for ringing the bell. We put the counting logic in a critical section (via ",[65,1285,1286],{},"ATOMIC_BLOCK()",") to make sure there weren’t any race conditions between writing and reading the count.",[1061,1289,1291],{"id":1290},"finishing-touches","Finishing Touches",[11,1293,1294,1295,1298],{},"The last part of the puzzle was getting the server to hit the webhook URL when users signed up. We decided to throttle to 5 seconds to avoid overwhelming the IFTTT API. So, after a signup, we essentially deferred POSTing for that amount of time, and then batched together signups into a count that we then passed through as ",[65,1296,1297],{},"value1"," in the request body to IFTTT.",[97,1300,99,1301,99,1304],{},[46,1302],{"src":1303,"alt":1195},"\u002Fimages\u002Fwriting\u002Fthe-coda-bell\u002Fbell_final.jpg",[105,1305,1306],{},"Not winning any design awards with this one, but it works great tucked away in a corner!",[11,1308,1309,1310,1315],{},"And there you have it! We had a blast making the Coda Bell, and I hope you enjoyed reading this. Feel free to adopt it yourself, and ",[15,1311,1314],{"href":1312,"rel":1313},"https:\u002F\u002Ftwitter.com\u002Fcoda_hq",[19],"tweet at us"," if you do! Lastly, reach out to me if you have any questions, and P.S., we’re hiring. :)",[1007,1317,1318],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":231,"searchDepth":232,"depth":232,"links":1320},[1321,1322,1323],{"id":1135,"depth":232,"text":1136},{"id":1248,"depth":232,"text":1249},{"id":1261,"depth":232,"text":1122},"2017-11-08","A few weeks ago, I got to be part of something special: the launch of Coda. To add a little ceremony to the occasion (it’s not every day you get to launch a company!), my coworker Chris Eck and I rigged up an old call bell to ring every time someone signed up. Here’s what it looked like:",{},"\u002Fwriting\u002Fthe-coda-bell",4,{"title":1036,"description":1325},"writing\u002Fthe-coda-bell","kLuFRTAj5ENSKteyvTjQx1Qyix0QDKecrAXSLvQQ6l4",1782034563929]