How Do You Even Write Secure Code Anyways

DeFi Security Summit 2022 · 28 Aug 2022 · 18:55

Samczsun asks how teams can actually write secure code. He starts with the JPL coding standard behind NASA's Curiosity rover and picks out rules that translate to Solidity, then offers five tips across the development process, including coding standards, a look at a bZx function that is very hard to review, and taint analysis that checks whether user-controlled data reaches a sensitive value. At DeFi Security Summit 2022.

Speakers

Topics

From DeFi Security Summit 2022, hosted by DeFi Security Summit.

Transcript

so next up we have uh Sam CZ Sun uh who's gonna be piping in well hey so I'm Sam unfortunately I can make it in person which means that uh I can't you know emphatically bang my head on the desk uh for emphasis but um today I will be here to talk to you about a question that is uh you know by the Numbers plug in your industry and that is how exactly do you even write secure code anyways uh and so you know obviously if we look at the if we look at the numbers it seems like this problem is kind of hard but um you know usually when we're faced with a hard problem it's worth checking if anyone else has solved this uh after all there's no point Reinventing the

wheel and for that I'd like to take a quick stint to talk to you about the Curiosity Rover this uh little project was proposed in 2004 and launched in 2011 and over that time uh NASA spent a total of about 3.2 billion or you know maybe like two or three D5 hacks uh on this project and with a team of about 30 people they wrote over 2.5 million lines of code in C no less and you know with the project this big NASA would obviously want to make sure that their investment wasn't going to be wasted and so you know they they did this by enforcing what's known as the JPL institutional coding standard for the C programming language now obviously this is a coding standard C and so some

things are going to be C specific but as it turns out there's actually a lot of things that can translate into solidity um for example here's a few that I have picked out of this uh these coding standards for example uh all code must be statically analyzed there must be no resulting warnings or errors even if you have false positives because the logic being if your code is complex enough to introduce a false positive then clearly something needs to change um you're not allowed to have any loops with Dynamic bounding you're not allowed to use recursion any declarations you make should be scoped as tightly as possible any return value should be used or explicitly discarded you should always validate every parameter at the start of every function you should always just use insertions and instead of standard mandates that uh if your function has more than 10 lines of code

and needs of at least one assertion if you have an expression that has more than one operation in it you can use parentheses to make the order of operations explicit and the last one is a real doozy it says that unless you have unless it's in special circumstances your functions should not be longer than 60 lines and so you can imagine that if you followed this guideline uh your blockchain protocol will be extremely secure the downside is that you know as you're following this guideline all of your developers uh will sort of look like this and so you know I think the real question that we should be asking is how do you write secure code anyways but you know for the regular person right and so that's what I'm here to talk to you about is I prepared uh five different ways that I think uh or five

different tips that I think would help uh you know throughout the entire developer process to sort of make sure that your code output is uh less susceptible to bugs and you know how to take the first one actually you know maybe coding standards are good and so I think to get a sense of just truly appreciate uh how coding standards can really elevate a code base I think it helps to take a look at uh one that does not and so here's a snippet of code that I've taken from a D5 protocol called bzx um you know this function at first glance might seem no short and sweet it's really nice but then you see that oh well we get this nice uh address array called Saint Patrick's and you just kind of have to memorize that uh

you know centers which is send addresses at three is the receiver and you get like five more unit values and you just have to memorize that values add to its toward interest and then uh once you do some you know high level validation you get you jump straight into the borrower trade and in fact I haven't bothered including the rest of the Snippets but if you're trying to review that code uh and you wanted to review the interest calculations you would actually jump from borrow a trade from pool into borrower trade into initialize interest into settle fee rewards for interest expense into pay fee reward into get another static call into another contract in other words good luck with that [Music] and so the question here is well how exactly do we write code that does spark joy and you know there's some simple guidelines here from things like using

descriptive variable names uh because you know this is 2022 and we aren't limited by we aren't limited by a characters per variable name we can limit that number of active variables for that code reviewers and Auditors don't have to keep as many things in context at once we can try to eliminate redundant logic and we'll get we'll get more to that later uh early exiting as much as possible uh we should put related code close to each other so that we don't have to travel with too far within the code to maintain context and of course we're all using git right we can go ahead and delete all that unused code to give you a sense of what that looks like here's a stupid code from a protocol that uh I think has much cleaner code maybe one of the uh cleanest uh code bases I've ever seen so

far and that is of course compound V2 um we can see here that you know we have this really nice almost linear uh progression of logic right as soon as we encounter a failure condition we immediately break out meaning that when someone's reviewing this code they can immediately see that you know by the time you get to say the line with uh that's a spiral index there's really only one way to get there that and that's to meet the two Phoenicians uh required above uh the only in fact the only bit of branch of logic we have is when we sort of calculate the repay and now um if the user specifies three pay negative one uh you know we see that every variable name you know is very descriptive we have replay about math errors about account borrowers Etc it's really I

think you know any auditor would be happy to read this code um borrowing of course for their their usual fee on the other hand you know you might come across something like this where we have this variable called essay which you know it's not immediately clear what that means it's you know it's actually some asserted scaled down we have this Boolean called check which gets set in both cases making it you know a bit redundant we have some code left over that has been commented out it's not really clear ah okay um yeah I mean here we're a bit short on time so let me just I'll just move on there but basically the idea is like you know if you if you have good coding centers it sort of makes sure that the code you're writing is uh easily not just so for other people both code

reviewers and Auditors uh to reviewing the code okay so let's try to get through the rest of this in the next nine minutes uh obviously testing is a huge deal but um you know testing is only effective if you're doing it correctly uh one example of not doing correctly is if you write tests uh that so you try to achieve 100 coverage and the reason for this is because coverage is not the end goal right if you're if you're writing tests purely to hit that hit that magical 100 coverage you're you're doing it wrong um recovery should help you to help guide you in finding the things that you want to test uh some good ways to actually you know some a good way to actually think about how to write tests is you know obviously you want to write positive unit tests

right these are unit tests that test that the function is actually doing what it's supposed to be doing um you know if you're writing a security reduction you should probably write some unit tests to make sure that it you know when you pass in four it returns two when you pass in uh nine returns 3 25 turns five right uh but you should also write some negative unit tests and I think this is the first place that people sort of slip up is that you know when you're writing a unit test for a square root function well what happens if you pass in six right what is square root of six of your function actually return is your function handle floating more numbers or does it return only integer it says around up or down um what if you pass in zero what does that return what you're passing negative

one do you handle negative numbers right okay and so writing tests for things that you expect to fail and a surgeon that actually does fail means that you're not only testing a happy case you're also making sure that if something bad happens uh the blast radius is limited yeah I think you don't know what integration tests are because you know your your D5 protocol isn't just going to be this one square root function it's going to be something that's going on I built on top of it like for example some sort of pricing algorithm and so an integration tests to make sure that the entire pricing works or the entire buy itself uh functionality Works means that you don't just make sure that individual building blocks are secure but you make sure your entire protocol works uh but before type test that I'm sort of

I I sort of came up with the name uh of this weekend um I'm I'm calling these active tests and what I mean by this is when you're writing code there's a there's a few things that you know you might assume is true right like if you're doing testing on local network you might sort of assume that uh you know the units of cool has sufficient liquidity right you're like I'm gonna sell that out because that that's not that's my control or you might assume that your chain link Oracle is always returning a price you might assume that your proxies are initialized uh but once you're a midnight these things might not necessarily be true and you know it is 2022 and we all have the technology to make made it forks and run tests I mean it Forex so why don't

we get in the habit of writing tests that insert that the conditions on Main net match what we expect right and then if these conditions ever don't match if one day we find that our implementation is not initialized because we call it implementation on our proxy contract we tried calling initialize and it worked well then hey that means that are active test failed and which probably could do something about it now I think the last two are fairly self-explanatory but I think once again unit tests are where people tend to split uh tend to slip up because it's not immediately clear to them how exactly you think about testing and so for that I'd first refer you to this wonderful tweet by Bill Seth which describes how a QA engineer might order a beer uh and I think we can take

inspiration from this by contemplating how Q engineer might test a factorial function uh if you're watching at home go ahead and pause the video if this is being recorded if you're not watching from home like right now um unfortunately you have no time to think about this because I'm going to jump straight into the answers how did you do well you know again we should obviously test the positive cases factorial of two of one zero uh what about negative one what exactly is a factorial of negative one what about a factorial of 1.5 uh if you give it a factorial of 2 to the 32 what do you get back do you get back to really not large number is it truncated to get back to maybe Infinity seeking Infinity what if you try to take the factorial of infinity or not a number what about the

factorial of a lesser right these are some things that you should be thinking about when you consider how to write this unit test for the simple factorial function uh but of course we're not limited to things as simple as a basic math function here's a quick excerpt from some code that allows you to buy a ticket it assigns you a ticket ID Returns the ticket ID and then there's this function that presumably uh lets you claim a price if one of your tickets is winning once again if you're watching from home go ahead and pause this otherwise I'm going to Jump Right In now the variable is called ticket so obviously we would pass in an array of tickets right if we pass an array containing one all of an articulate that's probably the way the valid way to call this what happens if we pass in our ticket without an array

what happened what does the function do or what if we pass in someone else's ticket can we claim a price for them if we pass into valid ticket it's a function revert probably passes our ticket twice do we get to clean the price choice right if you've done code review or if you sort of play the functions of your own that have similar logic you might be recognizing some patterns here and in fact as it turns out writing a unit test is often about recognizing these patterns for example whenever I see a numeric parameter the first thing I think is well what happens if I put a really big number in it does it break if I put in a really small number or any special numbers to to the function like zero or two to the N or maybe you know it's a token so one to the uh won't have

sent to the 18 stuff like that right if I see string parameter the first I think of is what if I put in an empty string or a really long string uh what if I put in malformed Unicode did you know that Unicode can be malformed or if I see an apps parameter you know the first thing I'm thinking of is or if I put in the zero address or what if I put in my own address or the address of a contract exploit in which acts almost exactly like the contract that the recall is expecting but just not quite in slightly several ways to let me steal the money um of course patterns don't only apply to the type of input the function is taking but to the function itself for example if you see a function that compares two things like a function that

lets you slash someone for providing a uh for double signing well what if you provide the same thing twice right just like you slash someone illegally or if you see a function that counts occurrences of something in an array like the number of times a sign or sign a value can you provide duplicating signatures right does that function do you do that if you see a function that accepts a reference to some object by an ID can you specify someone else's ID so you know clearly with the right mindset to unit testing with the right framework in mind uh you know unit testing and to a larger extent integration testing and now I'm calling active testing these can really help make sure that uh the code you're writing is secure in a way that's more than just sort of you know static code review

moving on we have two minutes left and I have two more things to talk to so we're going to try to really Breeze through these sorry uh sorry to the organizers uh sorry let's talk about building a solid foundation now looking at this bit of code we have uh we have this function that lets you upload a file uh this code will take that file name and the data and write it to disk uh so clearly the load bearing function here is this path details that standify is called because without that call if that call doesn't work correctly then you basically have this arbitrary file right vulnerability in this function in in this function right and as it turns out this usually it's a case right you have these helper functions which are fairly small but they typically process user input right like they'll validate some user input

they'll do something critical to it like safe math uh or they'll sanitize it and so you know these small functions are like very load-berry uh and you know in a sense this is good because small functions are actually the perfect candidate for unit tests so you have this you know very obvious Target that you should be uh you should be writing all of like the bulk of your test cases for right and so to give you an example of what I mean by this like here's another function this is uh the pricer function from ens right um this function is used to calculate how much someone should pay for their DNS name obviously one of the lower bearing functions here is calculating the length of the string uh because if you don't calculate that right then someone could potentially buy a really short name for a really cheap price

um but you know looking at this it's not exactly super trivial and so apart from just like reading the code and analyzing it you might want to write some uh unit tests for it and so you know you might consider testing Sterling of a just a regular string but also empty string or a with accent or this weird Arabic character that's you know super white on the screen but it as it turns out it's only one character or maybe even the poop emoji now of course it doesn't really help to just say as all helper functions because not all helper functions are that important you should particularly pay close attention to the helper function and set validate or sanitize user input it values using user input or really just do anything with use input and this

sort of leads me to the final Point uh I think we're probably about two minutes away here so here's one weird trick to improve security that Auditors don't want you to know about um here's this function from this uh this code base I reviewed a long time ago uh this is a function that lets you mint these X tokens right you deposit some some asset uh called D2 assets and then you get minted some X tokens and so uh you know if you're if you don't know how to code review in the past the first thing you might be wondering is like how can we what exactly is the return value of x token right like how can we control what token this is minting and it turns out that function X token just returns this field X token and the field Expo can get set in this other function call

uh by this value X token address and so now you're probably thinking okay so how exactly can I make the owner call the set X token function with a controlled X token address and the answer to that is this other function called create vault which calls both settings of an address and settings token and so now you're probably thinking okay so if I pass in any X token address even one that already exists then I can set that X token set my Vault ID to use that X token but then I can pass in any other asset and so maybe I'll pass in like a really worthless asset uh and pair that with like a really valuable X token and now I can deposit my really worthless asset and get my X token back and the point I was trying to illustrate

with this example is this concept of sources and sinks right uh on a really big code base it's pretty hard to go through that entire code base and review everything line by line so you should really prioritize the things that an attacker can affect right and the way you do that is you consider the fact that your code base will be the same no matter how your no matter you know if it's today or tomorrow if it's one person or another really the only the only time your code base will change in its behavior is based on the input right and so the way that the attacker is going to make your code go phase do something unexpected is by providing some malicious input so you can you can mentionally do what's known as taint analysis right you start with any uh any source that is to say any uh anywhere

that data is uh consumed from from the user and you still try to find all the ways that I didn't get to a sync which is any place an important operation is occurring right and so we say that you know if your data comes from a source is Tainted and then if your data uh you know trans Traverse this into a different function or is mutated or transformed in some way it's still tainted and it maintains its change status until you validate it at some point right you make sure that it's within the balance you expect you make sure that it's in address you expect stuff like that right and once you validated it this data is no longer tainted and so with this model you can see how in the previous example we started from a sync which is this place that the X token was minted and we sort

of tried to figure out you know this critical bit of data this x token address right was there any path where actually this data was tainted by a user and it turns out that there was uh in the create Vault function and so that sort of wraps it up for uh my let's talk on you know how exactly did you write secure code uh normally I'd open up their questions but I see we're 83 minutes over so I think maybe we just hop uh right into the panel and then uh we can go from there I guess