PowerShell - Testing endpoints that perform Anti-forgery verification

Published May 29, 2019 by FoxDeploy

First off, big thanks go to 🐦Ryan Ephgrave, an incredibly talented and easy to work with PowerShell and dotnet god I have the pleasure to learn from over at #BigBank™ (its a great thing LinkedIn doesn’t exist…)

We had a situation arise recently where we needed to create some Integration tests in Pester to validate a long list of web pages to be sure they responded after a deployment.  I started out manually writing a litany of Pester tests by hand like this:

Context 'Post Deployment Validation' {
    It 'Website #1 should be accessible' {
        $url = 'https://someserver:someport/someEndpoint'
        $results = Invoke-WebRequest -Uri $url -UseDefaultCredentials
        $results.StatusCode | should be 200
    }
 
    It 'Website #2 should be accessible' {
        $url = 'https://someOtherserver:someport/someEndpoint'
        $results = Invoke-WebRequest -Uri $url -UseDefaultCredentials
        $results.StatusCode | should be 200
    }[...]
}

I spoke with the team about what I was doing and Ryan drew my attention to the very neat TestCases of Pester, which you can read more about here.

With a bit of work, I converted my long list of tests (which I typed by hand…why?  Because I finally got a PS4 and I stayed up too late playing Sekiro!) into a JSON file like this.

 [
    {
        "SiteName" : "Our Home Page",
        "Url" : "https://someserver:someport/someEndpoint"       
    },
    {
        "SiteName" : "Our WebApp #1",
        "Url" : "https://someOtherserver:someport/someEndpoint"       
    }
]

Then to hook this up to our Pester test from before and…

Context 'Post Deployment Validation' {
    $EndPointList = Get-Content $PSScriptRoot\Endpointlist.json | ConvertFrom-Json
    $paramArray = @()
    ForEach($instance in $EndPointList){
        $paramArray+= @{
            'SiteName' = $instance.SiteName
            'URL' = $instance.URL
        }
    }
 
    It '<SiteName> should be accessible' -TestCases $paramArray {
        Param(
            [string]$EndpointName,
            [string]$URL
        )
 
        $results = Invoke-WebRequest -Uri $url -UseDefaultCredentials
        $results.StatusCode | should be 200
    }
}

Then we run it to see…

But what about the post title?

You guys, always sticklers for details.  So this covered a lot of our use cases but didn’t cover an important one, that of making sure that one of our internal apps worked after a new deployment. It was a generic MVC app where an authorized user could enter some information and click a button to perform some automation after an approval process.

The issue was that as you could imagine, security is a concern, so time has been spent hardening tools against attacks like Cross-Site Request Forgery attacks.  Which is great and all, but made automated testing a pain, namely because any attempt I made to submit a test request resulted in an error of one of the following:

The required anti-forgery form field __RequestVerificationToken is not present.

The required anti-forgery cookie __RequestVerificationToken is not present

So what’s a dev to do?  Send a PR to disable security features?  Create some new super group who isn’t subject to the normal processes, just used for testing?

Of course not!

How MVC Antiforgery Tokens work

Any good production app is going to first and foremost use AspNet.Identity and some kind of user authorization system to ensure that only approved users have permission to use these tools.  If you don’t anyone who can route to the web app can use it.  This is bad.

So let’s assume we’ve done our diligence and we have our web app.  A user has permission to the app and they’re following safe browsing behavior.

Let’s imagine the app is a simple user management app, something like this, which has a simple class of Users, perhaps with a field to track if they have admin rights or not.

class FoxDeployUser
{
    public String UserName {get;set;}
    public String SamAccountName {get;set;}
    public bool IsManager {get;set;}
}

Now imagine if your user account has administrative rights to make changes to this system. If so, your account could easily navigate to a Users/Edit endpoint, where you’d be prompted with a simple form like this to make changes to a user account.

The scary thing…if the account we are using for this portal is always permitted, and doesn’t have a log in process, then any site while we are browsing the web could make a change to this portal.

Here’s how it would work, assume I want to make a change to this user.  I load up the /Users/Stephen endpoint and type in my values and hit Save, right?  What happens in the background (And which we can see in Chrome Dev tools) is that a form Submission is completed.

It simply POSTS back to the web server the contents of a form.  And you know what else?  Any website you visit can contain JavaScript that performs the exact same kind of AJAX Post to the web server.  There are even JavaScript utilities that will automatically discover webservers on your network.  So with this in mind, imagine visiting a webpage that looks pretty innocuous:

Black mode = evil website[/caption]

Clicking the Post button there will send an AJAX Post formatted like the following:

$("button").click(function (e) {
            e.preventDefault();
            $.ajax({
                type: "POST",
                url: "https://MyInternalApp:44352/Users/Edit/1",
                data: {
                    UserID: 1,
                    UserName: "Stephen",
                    SamAccountName: "AD-ENT\\Stephen",
                    IsManager: "true"
                },
                success: function (result) {
                    alert('ok');
                },
                error: function (result) {
                    alert('error');
                }
            });
        });

So this is an attack from one-website, through the user’s PC, to another website they have access to!

Will it work? If it does, I’ll click the button from one site and we’ll see the user’s ‘InManager’ property change in the other site.

Wow that’s terrifying

Yep, I thought so too.  Fortunately for all of us, there are a lot of ways to mitigate this attack, and most MVC frameworks (citation needed) ship with them out of the box.  In ASP.net MVC you signal that we should protect an endpoint against a CRSF attack by adding this Annotation to the method.

// POST: Users/Edit/5
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, [Bind("UserID,UserName,SamAccountName,IsManager")] User user)
        {

This adds a novel little hidden form box to the UI which contains a one-time use token, embedded in both the form and the cookies.

Here’s an example of the normally hidden element, which I’ve revealed using Chrome Dev tools.

Now if I attempt to submit this form, I’ll encounter an error, since my attack won’t be able to retrieve the form as the user, get the cookies, and then repost back to the endpoint.  Since my post won’t have the one-time code needed to do this, it will be rejected at the Controller level.

Testing an endpoint which has CSRF Protection

Now, to the meat of the issue.  As part of my Test Suite, I need to run a post through this endpoint and validate that the service after an update is able to perform this business function.

I can do this by maintaining a PowerShell WebSession to get the matching cookies and then submit them using Invoke-RestMethod.

Describe 'WebApp Testing' {
$Request = Invoke-WebRequest -Uri https://someserver:someport/Users -SessionVariable Session -UseBasicParsing -UseDefaultCredentials
$TokenValue = ''
ForEach($field in $Request.InputFields){
    if ($field.Name -eq '__RequestVerificationToken'){
        $TokenValue = $field.value
    }
}
 
$header = @{
    '__RequestVerificationToken' = $TokenValue
}
 
$fields = @{
    '__RequestVerificationToken' = $TokenValue
    'UserName' = 'TestUser'
    'SamAccountName' = 'QA\TestUser'
    'IsManager' = $false
}
 
It 'WebApp1 : Should edit a User' {
    $Response = Invoke-WebRequest -Uri https://someserver:someport/Users -SessionVariable Session `
        -Method Post -UseBasicParsing -UseDefaultCredentials -Body $fields -Headers $header
    $Response.StatusCode | should be 200
}
 
It 'WebApp1 : Should throw when the user has no token' {
    {Invoke-WebRequest -Uri https://someserver:someport/Users `
        -Method Post -UseBasicParsing -UseDefaultCredentials -Body $fields -Headers $header } | should throw    
}
}

My first integration tests.  I’m so proud.  And I’m also kind of ashamed too, because up to this point I’d been manually loading two dozen web pages and making requests by hand to validate deployments.

Thanks for reading!


Microsoft MVP

Five time Microsoft MVP, and now I work for the mothership


Need Help?

Get help much faster on our new dedicated Subreddit!

depicts a crowd of people in a night club with colored lights and says 'join the foxdeploy subrreddit today'


Blog Series
series_sml_IntroToDsc
series_sml_PowerShellGUI series_sml_IntroToRaspberryPi Programming series_sml_IntroToWindows Remote Management Series The Logo for System Center Configuration Manager is displayed here Depicts a road sign saying 'Learning PowerShell Autocomplete'




Blog Stats