Extension cmdlets for IIS – done right way

In my previous post I explained how to write your custom commands, that are based on lower level cmdlets. Well, there is a problem with this approach. As soon as we call InvokeScript() passing command line as string, PowerShell interpreter will process is exactly the same way as if it would be entered by user in console window. If command line will have parameters, which are embedded script blocks, those will be executed. This is not secure, because you cannot control where input is coming from. People name this security issue “script injection”. If user enters command line with script blocks from keyboard, it is up to him/her to decide, is it safe to run or not. In our case we are taking parameters and building new command line. What if parameter is coming from the file on disk, or from outside world? It could be unpleasant surprise to see side effects of your script.

Let me explain it a bit more. Say, we are dealing with cmdlet ‘new-webvirtualdirectory’.

get-command new-webvirtualdirectory | select definition | fl *

Definition : New-WebVirtualDirectory [-Name] <String> [-Site <String>] [-Application <String>] [-PhysicalPath <String>] [-Force] ...

As we could see we have number of string parameters. Our code in snapin packages it into new command and calls InvokeScript API, passing this command as parameter. Any of string parameters could be a point for script injection. Let’s say we got Name like that:

$name = "$(remove-itemproperty hklm:\software\microsoft\powershell\1\PowerShellSnapIns\webadministration -name assemblyname)"

If then we will use this in our command, resulting base command will be

new-item –name $(remove-itemproperty hklm:\software\microsoft\powershell\1\PowerShellSnapIns\webadministration -name assemblyname) –type …

When PowerShell will process it, it will execute script block, and will delete registry key. Sure, cmdlet will fail after that, but damage is already done. This is a trivial example of script injection.

How could we avoid this? There is a way. We have to deal with command objects instead of string command line. PowerShell provides set of APIs and types that could be used to get engine runtime, create a pipeline and command, fill command parameters, add it to runtime and then execute. I will show you how to do it.

From this point we are dealing with C# code.

First, we will need a runtime environment that already has our snapin or module added, otherwise we cannot use any IIS commands. Runtime in PowerShell is named “runspace”. In this runspace we will need a create pipeline for our command.

Pipeline pipe = System.Management.Automation.Runspaces.Runspace.DefaultRunspace.CreateNestedPipeline();

Then we have to create command that we want to execute. And add parameters that we declared for our cmdlet.

Command cmd = new Command("new-item");
cmd.Parameters.Add("type", StringLiterals.VDirObjectTag);
StringBuilder pathBuffer = new StringBuilder("IIS:\\sites\\" + siteName);
if (!String.IsNullOrEmpty(appName))
{
    pathBuffer.Append("\\" + appName);
}
pathBuffer.Append("\\" + vdirName);
cmd.Parameters.Add("path", pathBuffer.ToString());
if (!String.IsNullOrEmpty(homePath))
{
    cmd.Parameters.Add("physicalPath", homePath);
}
if (Force.IsPresent)
{
    cmd.Parameters.Add("force");
}

Finally, command should be added to pipeline and executed.

pipe.Commands.Add(cmd);
Collection<PSObject> result = Utility.InvokePipeShowError(this, pipe);
foreach (PSObject res in result)
{
    WriteObject(res);
}

That’s it.

I will discuss function InvokePipeShowError in a moment. When PowerShell executes command this way, it doesn’t try to parse or interpret any parameters, and script injection is not possible.

Why then I used this questionable code in my previous post? Well, at that moment I didn’t know how to obtain PowerShell runspace containing my snapin (or module, if we are in version 2.0 environment), James Brundage from PowerShell team came to the rescue and showed me how it could be done. After that I rewrote all cmdlets that were using InvokeScript.

There is one subtle problem left. If synthesized cmdlet fails, PowerShell v1.0 doesn’t output error, no matter how you call InvokePipe(). This is why I am using my own utility function that checks for errors and prints them out. Here is this function:

public static Collection<PSObject>InvokePipeShowError(
    PSCmdlet cmd,
    Pipeline pipe
)
{
    Collection<PSObject> confirmPref = cmd.InvokeProvider.Item.Get("variable:\\ConfirmPreference");
    cmd.InvokeProvider.Item.Set("variable:\\ConfirmPreference", "High");
    Collection<PSObject> result = pipe.Invoke();
    foreach (PSObject pref in confirmPref)
    {
        cmd.InvokeProvider.Item.Set("variable:\\ConfirmPreference", pref.Properties["Value"].Value);
    }
    if (pipe.Error.Count > 0)
    {
        Collection<object> errors = pipe.Error.ReadToEnd();
        foreach (object error in errors)
        {
            PSObject errObj = error as PSObject;
            if (errObj != null)
            {
                cmd.WriteError(errObj.BaseObject as ErrorRecord);
            }
        }
    }
    return result;
}   

This method temporarily sets confirmation preference in PowerShell to "High", then executes commands on pipeline and checks, if there are any errors added, prints errors and returns.

 

Happy scripting!

Sergei Antonov

No Comments