About this talk
In this talk, the speaker Irina discusses how modern applications can still possess security vulnerabilities despite leveraging best practices and modern web patterns. She elaborates on the challenges posed by refactoring legacy code, which often leads to broken assumptions related to security contracts. Irina focuses on practical examples, emphasizing the importance of validation and unified authentication methods. She illustrates these concepts with real-world scenarios and vulnerabilities, including flaws in input validation and authentication processes that can lead to significant security issues. By analyzing common pitfalls and providing insights, she encourages developers to thoroughly assess their code paths and maintain rigorous testing to prevent such vulnerabilities.
Full transcript
Hello everyone. No. Hi. Do you hear me? Okay, many people hear me, not all of you. Um I'm not sure if I'm supposed to talk in >> [laughter] >> not loud. Okay, I guess you all have headphones. Let's start. Okay. Uh thank you for joining my uh talk today. I'm super excited to be here with you and uh today we're going to discuss how modern applications can
still have critical security violations and vulnerabilities despite using best practices and modern web patterns. And before we start, I would like to quickly introduce myself. My name is Irina. I'm a master student and finishing in a month. Very nervous and stressed. I'm a former Black Hat presenter and I'm a security researcher. I focus at finding confusion vulnerabilities in in code and that's how this uh presentation kind
of become to life. So, how many of you here uh work with the code that is about 5 years old or even older? Quick show of hands. Yeah, okay. Wow, so many people. How many of you have committed or not committed in this code within 6 months? Wow, so little people. You're so productive. Um so, if I tell you that your boss comes tomorrow and tells you
that this code in this code there was found a very crucial critical vulnerability, would you be surprised? Probably not, because Uh, sorry, I'm super [clears throat] nervous. Probably you wouldn't be surprised because this code contains uh legacy code. It is very hard to debug, maintain, and um it's tightly coupled and just a very uh a very nice hanging fruit for attackers to exploit and find vulnerabilities. But
what if I told you that uh the code that you shipped last week where you used modern stack, best practices, all security checks passed, and still there was a vulnerability. That's something we're going to talk about today. So, the problem is that when you are uh following best practices and refactor your code, um you kind of face a problem with uh assumptions. When you have a very
big chunk of code that is like 100 lines of code, it does uh nobody knows what, but it's uh in the one place, there are assumptions that you might not have seen. And when you refactor it, which is a very good practice to uh split it in lower in smaller chunks, make code more maintainable and cleaner, and reduced your um the overhead, um that's a good that's
a good practice, but what can happen is that you skip your assumptions, and you might have you might miss the assumptions. And uh the problem is that the security contract that existed in the code before refactoring might be broken because of the because of something you didn't see in the first place. And I'm talking mostly about the request values, authorization, authentication, what values you check and what
you use later, what subject what subject you authenticate and what actually passes, what object authorizes, and so much more. So, today we're going to talk about it in more details. We're going to look at two use cases, modern and common practices such as moving your validation out of handler, unified authentication, and at the end you will see a real vulnerabilities that were introduced despite following these principles.
So, principle number one, move validation out of handler. So, when you have handler which is like super big, enormous, it does it has so many responsibilities, validates input, it authenticates users, authorizes something, and also performs business logic, it's kind of time to move on and to uh split your code into the middleware and into the handler. And the middleware is some sort of function that is going
to do all the security checks that you need such that your handler is going to be clean, maintainable, and you will always know what happened there. It's a good practice. We all use it and uh let's see an example of how when and how it can be done. So, imagine a scenario, you are a developer and you're developing a delivery application. And in your in your uh
checkpoint endpoint, you are accepting the validating it and then using it. It's a very simple example in Flask, uh so we we stay on the same page, but the patterns I'm going to talk about, they can spread across any framework and they can introduce in many ways. So, bear with me, let's see how Let's see how we can make it worse. So, basically You have You have
your validation, you have your business logic. So, you want to separate it. You're creating the middleware that is always happening before the request. You're extracting the tip, you're checking that it's not negative. And then, after it is validated, you can use it at the checkout when you create the order. of course, you want to generalize because you don't know how many times like where are you going
to use your tip? Where are you going to apply it? So, you need to validate it and you need to check the tip across all of your accessors. And uh let's say we're using uh function get parameter that checks tip from the query string, from JSON body, from form body. Nice and clean. So, everything is validated and then we use it. What's the problem? The problem is
you have You kind of have an idea that your middleware takes the tip value and checks it in the um and checks it everywhere, while in the reality, it only checks the first place where tip is present. In this example, we are taking first the query string, then JSON in form. But using the um checkout endpoint, you're only taking your tips from one specific place. So, do
you see the problem? The problem is that before we had one assumption that tip is is coming from one accessor, we're validating it, and we're using it. And when we try to generalize, we broke this assumption. Let's see an example of the exploit. So, a more naive attacker tries to steal money and uh just sends a negative tip to see whether whether she can get anything out
of it. And of course, our application says, "No, bad request. Uh go home. It's not happening." Which is nice. It's what we want. But if a more experienced attacker sends tip in two places, in the query string and in the JSON body then we allow it and we take >> [clears throat] >> and apply negative tip. Basically, giving our money to nice So, what actually happens? Again,
your validator, because it's generalized, it takes the value from the query string, but your business logic only applies it to the JSON body. So, middleware reads from tip 20. It's correct. It's It's positive, of course, I validated it. And since this is a validated handler, trust middleware and says, "Of course it's validated, then I just apply it." the problem is not with this uh you may say
stupid or I don't know, not realistic code. The problem is that we had some sort of assumptions that we broke without realizing that the security contract was that we take the same value from validating and applying it later. So, the thing is both handler and middleware are correct. They both They are both acting as expected regarding their own assumptions. But when they are combined together, they break.
Which leads to financial loss and uh yeah, bad reputation. But you say, "Okay, this is just some stupid example. Why should I care?" The thing is it's way more common than you can imagine. And we're going to see the second case uh example. And uh then when you see how it's really applied in real code, I don't know, see the pattern. second second common pattern is to
unify authentication. So, imagine you just started your startup or food delivery application, you're working for someone, and you're only using um Yeah, you're only authenticating web users. So, when you have different endpoints and functions, you are using cookie session to authenticate them. But then your business grows, and you want to now support me microservices or uh API and you are adding more authentication methods JWT. And now
you're growing even more and you need more authentication methods. What is the problem? Not only you have to maintain where exactly do you use what methods. Once you introduce a new authentication method, you have to repeat it. So, I hope you can understand that okay, don't repeat yourself. It's uh it's very common to do something like this. So, instead of repeating uh authorization in every endpoint, we
are creating some sort of common class or helper function, again middleware or anything that can do it in one place and and uh the endpoints can use it for validation. Uh again, you might say that um nobody does it anymore. It's not It's not modern. I don't write this. But the idea is not how you implement it, but what exactly do you do. And if you don't
do I don't know maybe a common class and children, it doesn't mean you are safe against such uh problems. So, um let's take an example. Now I'm using SDS. Uh so we have a bit more I don't know more examples. So, what happens here? We are creating an order. We are using uh authentication method. We're checking the user that's coming from the request. If it's not the
email that I authenticated, go home. I don't care about you. But imagine we have so many endpoints that has to do authentication for all uh purposes. Let's create a common class. Let's create a customer house guard that's going to check the Uh it's going to check from cookie and it's going to check the credentials. And then we're going to see Yeah, is it the same person? Should
be fine. Should be clean, should be good. And now our login uh also uses the guard because why not? We want to authenticate users, we need to reuse our abstractions. So, what it does, it uh uses customer off guard to authenticate, and if user is authenticated, I'm setting the session uh with the user ID to the email I I get from the body. Again, do you see
the pattern here? The problem is that our guard was taking from the cookie and then from the credentials. Again, as previously we saw uh use case number one. But, our login form only takes this the uh data the value from JSON. And you might say, "Okay, so just just use values across all components. It's no big deal." The thing is, while I'm showing you these examples with
uh input validation problems, there are this class of vulnerabilities are much bigger than you can expect. And on saying that, let's see some exploits and examples. So, again, uh naive attacker tries to log in as a legitimate user with wrong password, and our application correctly says, "Bad request. Unauthorized." Yeah. Just Just don't do this anymore, please. But, if I as an attacker try to If I as
an attacker first authenticate as my user, legitimate, my password, my credentials, everything, and then I save my cookie session, I try to uh log in as a legitimate user using the JSON body. And now our login succeeded. And now I can read their information, I don't know, get their card number, whatever. I can do whatever I want because I took their account. I'm interested whether it's kind
of get like makes more sense, whether it's too easy or whether it's going to be too hard. But let's see what happened actually. So again, because we had a kind of common class that authenticated the user, it checked first from the cookie and second from the body. And then we had a business logic that applied our login, that applied only for the JSON body. And the middleware
kind of asked, um is this request authenticated? Like is it already authenticated? Does it exist? Does it not? What does the login actually says? Are these credentials authenticated? And that's where the confusion is coming from. And again, what was the problem? You can say, okay, so the problem is that when we extract something with a common class middleware, some sort of functions that components reuse, just make
sure that everyone reuses the same thing. No big deal. The thing is the it's not as simple. Again, the security contract that was broken was that we authenticate and use uh the same identity. And again, the identities were not matching even though both components were correct on their own. uh yeah, as an impact we have uh account takeover, broken authentication, bad reputation, your business kind of failed.
I'm sorry. Um but again, what's the problem with it? Is it really that common? Is it really that bad? So I'm going to show you some real world cases. So Apache HTTP server, I hope many of you are familiar with it. Uh, actually it suffered from a very critical, yeah, 8.8 or 9.8 is very um, vulnerability that allowed unauthenticated attackers to use sensitive information. Basically, the vulnerability
was, uh, path reversal and remote code execution. Let's see what happened here. Apache, uh, had to introduce a new >> [snorts] >> new kind of fix of checking that the request that was coming uh, didn't didn't contain any path reversal pattern. And, uh, what happened is that request went through traversal check. Then, if it if it was correct, it went went to the file access, and then
we had the response. The only problem is like it didn't work like that. What happened in reality is that we also had URL decoding, uh, function that was applied after the traversal So, what happened is that uh, traversal check didn't pass uh, A / B or A / 2.2.B because it's, uh, path reversal pattern. But, if you encode it your dot at least once or like just
once then the traversal check explicitly explicitly checked whether % 2E is the same as dot. It's not. Good job, guys. We are so good. We pass the check. It's validated. But, then you URL decoding check, uh, took the uh, took decoded it back to two dots, allowing path reversal. Neat. But, again, it's not that big of a deal, right? Okay, they tried to refactor. They kind of
slipped. Didn't work out. Happened to to all of us, right? And they patched it in the next version. The thing is they didn't. So, what happened is that they introduced a new They introduced a patch uh with an idea that I'm only doing URL decoding after uh traversal check. It's a nice uh it's a nice pattern. I want to reuse it. So, the only problem is that
I am using decoding only once. So, I guess I need to use it twice. So, they implemented it and yeah, it worked. The um this string didn't pass because they were checking it. And uh the threat model was kind of clear. So, if I would be a a developer, I would also think I would also think that, wait. If I have like a vulnerability where I am
incorrectly decoding um my request based on uh how do they pass through what uh components, I would also say, okay. So, whenever I have some request passing through components, just decode it before it pass through this component, through this function. Quite clear. I'm curious whether any of you can see the problem here. Just uh raise your hand if you see what the problem. Okay, one. Okay, many
of you. Okay. Okay, good. I'm glad some people kind of get the idea. So, the problem is that if you >> encode it twice, now the traversal check was like, oh my god. Uh percent percent 32 percent 65 is not uh the same as percent 2E and it's not the same as dot. Then I'm passing it all correct and all clear. that's not enough. Why is this
vulnerability even could happen in the first place? Because we applied decoding twice. Because when you have this uh this kind of string coming. You apply You apply it once and uh first it can pass, but then you apply it once which becomes you got and that's basically what happened. the main problem wasn't that how many times you apply it. Okay, then let's apply two times or five
times or I don't know, 100 times. We can go on and on and on with that idea. The problem is that how many times do you apply it to the components? Because if you apply it to the component once then it's fine. Unless you apply it once for every component. Because the problem here was that uh while it was passing through the traversal it applied it once,
but through the file execution function it applied it twice. And again, we have a broken assumption. We had an assumption that if we just decoded it, it's going to be secure, but we didn't check for how many times do we do this. And uh it cost the party I think a lot of money, so yeah. So, um what can we do about this? What are the practical
advices I can tell you by the end of this uh presentation is when you're refactoring something, you always have to check what assumptions are you splitting, how your code paths can diverge depending on what input do you pass through this. And uh it can manifest as input value. I don't know, is there is a heap or is authentication, is normalization, or even as what uh type of
what part of the list are you checking. For example uh one component can check the first part of the list and the the other one the last one. Or it can Yeah, it can do many things. It's a very broad uh class of vulnerabilities and to make sure you never get this in your own code, you have to first of all make good test, of course. The
second of all, try to always understand how your code path can diverge depending on the input. So, yeah. Um so, that's kind of all that. Thanks so much for coming today. My name like yeah, you can see more examples of these vulnerabilities on the GitHub. And uh you can also contact me if you want to chat about the or anything. And uh if you guys have any
questions, I'm I want to ask answer.