Tag: PHP

Fortify on Demand Remediation – SQL Injection

This vulnerability occurs when you accept user input and then use that input in a SQL query. The basic remediation is to use oci_bind_by_name to bind variables into placeholders.

A query using an equivalence clause

The simplest case is a query with an equivalence clause.

The code:

        $strQuery = "SELECT DISTINCT EXCHANGE_CARRIER_CIRCUIT_ID, CIRCUIT_DESIGN_ID FROM circuit$strDBLink WHERE EXCHANGE_CARRIER_CIRCUIT_ID = '$strECCKT' ORDER BY CIRCUIT_DESIGN_ID";
        $stmt = oci_parse($kpiprd_conn, $strQuery);
        oci_set_prefetch($stmt, 300);
        oci_execute($stmt);

Becomes:

        $strQuery = "SELECT DISTINCT EXCHANGE_CARRIER_CIRCUIT_ID, CIRCUIT_DESIGN_ID FROM circuit$strDBLink WHERE EXCHANGE_CARRIER_CIRCUIT_ID IN :ecckt ORDER BY CIRCUIT_DESIGN_ID";
        $stmt = oci_parse($kpiprd_conn, $strQuery);
        oci_bind_by_name($stmt, ':ecckt', $strECCKT);
        oci_set_prefetch($stmt, 300);
        oci_execute($stmt);

The same placeholder can be used with the like query. Use “select something from table where columnname like :placeholdername” followed by an oci_bind_by_name($stmt, “:placeholdername”, $strPlaceholderVariable).

A query using an IN clause

– is a little tricker. You could iterate through the array of values and build :placeholder1, :placeholder2, …, :placeholdern and then iterate through the array of values again to bind each value to its corresponding placeholder. A cleaner approach is to use an Oracle collection ($coll in this example) and binding the collection to a single placeholder.

            $arrayCircuitNames = array('L101 /T1    /ELYROHU0012/ELYROHXA32C','111  /ST01  /CHMPILCPF01/CHMPILCPHH3','C102 /OC12  /PHLAPAFG-19/PHLAPAFGW22')
            $strQuery = "SELECT CIRCUIT_DESIGN_ID, EXCHANGE_CARRIER_CIRCUIT_ID  FROM circuit$strDBLink  WHERE EXCHANGE_CARRIER_CIRCUIT_ID in (SELECT column_value FROM table(:myIds))";            $stmt = oci_parse($kpiprd_conn, $strQuery);
            $coll = oci_new_collection($kpiprd_conn, 'ODCIVARCHAR2LIST','SYS');
            foreach ($arrayCircuitNames as $key) {
               $coll->append($key);
            }
            oci_bind_by_name($stmt, ':myIds', $coll, -1, OCI_B_NTY);
            oci_set_prefetch($stmt, 300);
            oci_execute($stmt);
Queries with multiple LIKE conditions

Queries with an OR’d group of LIKE clauses can be handled in a similar fashion – either iterate through the array twice or create a collection with strings that include the wildcard characters, then bind that collection to a single placeholder. Create a semi-join using an EXISTS predicate

            $arrayLocs = array('ERIEPAXE%', 'HNCKOHXA%', 'LTRKARXK%');
            $strQuery = "select location_id, clli_code from network_location$strDBLink where exists (select 1 from TABLE(:likelocs) where clli_code like column_value) order by clli_code";
            $stmt = oci_parse($kpiprd_conn, $strQuery);

            $coll = oci_new_collection($kpiprd_conn, 'ODCIVARCHAR2LIST','SYS');
            foreach ($arrayLocs as $strLocation) {
                $coll->append($strLocation);
            }
            oci_bind_by_name($stmt, ':likelocs', $coll, -1, OCI_B_NTY);
            oci_execute($stmt);
A query using DUAL

Queries where values are selected from DUAL – In some of my recursive queries, I need to include the original input in the result set (particularly, this query finds all equipment mounted under a specific equipment ID – I want to include the input equipment ID as well). Having a bunch of ‘select 12345 from dual’ is fine until I need to use placeholders. This is another place where the collection can be leveraged:

     select column_value equipment_id from TABLE(sys.ODCIVARCHAR2LIST('12345CDE', '23456BCD', '34567ABC') );

Adds each of the values to my result set.

Which means I can use a query like “select column_value as equipment_id from TABLE(:myIDs)” and bind the collection to :myIDs.

Fortify on Demand Remediation – Cookie Security: Overly Broad

Cookies are tied to hostnames and paths. This vulnerability occurs because not every site is its own hostname – when you own all of application.example.com/*, this is basically a false positive. But, if you host your app on a shared URL (e.g. look at www.rushworth.us/lisa and www.rushworth.us/scott and www.rushworth.us/owa … these are three different sites on my home web server. Scott, on is web site, could set a cookie with a path of “/” that uses the same name as a cookie I use on my site – my site would then use the stuff Scott stored through his site. Not such a problem in our scenarios, but a huge problem if you’re talking about a hundred different people’s blogs on some shared domain.

To sort the vulnerability, set the cookie path to something more than /

Fortify on Demand Remediation – LDAP Injection

If you build an LDAP search criterion from user input, it’s possible for the user to inject unexpected content into the search. If I say my username is lisa)(cn=*) or lisa)(|(cn=*) … a filter of (sAMAccountName=$strUserInput) becomes something unexpected.

In php, there’s a filter to escape LDAP search filters – use ldap_escape()

$scriteria=ldap_escape("(&($strUIDAttr=$strUserLogonID))", null, LDAP_ESCAPE_FILTER);

Fortify on Demand Remediation – Header Injection: Cookies

Cookie injection vulnerabilities occur when user input is stored into a cookie. It’s possible for malicious input to include newline characters that would be parsed out as new elements in the cookie. As an example, if I send my user ID as “lisa\r\nadmin: true” … I’ve got a cookie that says the userID is lisa and admin is true.

With Fortify on Demand, you cannot just filter out \r and \n characters – Fortify still says the code is vulnerable. You can, however, filter out anything apart from alpha-numeric characters (and, I assume, any oddball character that has a legit reason to be included in the user input):

$strLogonUserID = filter_var(preg_replace(‘/[^a-z\d_]/iu’, ”, $_POST[‘strUID’]), FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);

PHP oci_bind_by_name Caveat (a.k.a. a reminder to read the documentation)

This is readily apparent when you actually read the documentation for oci_bind_by_name … but, if you quickly skim through the doc and get mostly what it’s telling you, you can lose data when implementing oci_bind_by_name in your code. That’s because the variable that gets bound into the name is a reference. It’s right there in the description

And again in the more verbose part of the description

Well? They really mean It!

I usually iterate through arrays with for $x=0; $x<count($arrayOfData);$x++ — which is fine because $arrayOfData[$x] is a valid location in memory that contains the data I want. I had a program, though, that accepted user input. Input that frequently contains a bunch of extraneous newline characters in the middle of the data. To avoid making the users clean up their input, I just threw an array_filter on the input. But I don’t need the reorganized data, so I didn’t “waste” the cycles to re-index the filtered array. Instead, I used

$iIterator=0;
for( $itemInput in array_filter( $arrayOfData ) ) {
     oci_bind_by_name($stmt, ‘:placeholder’.$iIterator++, $itemInput);
}

Which is fine in most circumstances. But, with oci_bind_by_name … the memory address where $inputItem happened to be stashed is bound to the names :placeholder0, :placeholder1, …, :placeholdern – that memory address happened to still contain the last element of arrayOfData I happened to stash there because the server isn’t so busy that memory is being reallocated a fraction of a second later. But this loop does not bind each element of arrayOfData to its corresponding placeholder — instead of saying “select * from MyTable where ID IN (1,2,3,4,5)” … I had “select * from MyTable where ID IN (5,5,5,5,5)”.

In this case you need to use the array index

for( $x = 0; $x < count( $arrayOfData ); $x++ ) {
     $strPlaceholderName = ":inputclli".$x;
     oci_bind_by_name($stmt, $strPlaceholderName, $arrayOfData[$x]);
}

So each unique memory address is bound to a placeholder.

Fortify on Demand Remediation – JSON Injection

This vulnerability occurs when you write unvalidated input to JSON. A common scenario would be using an Ajax call to pass a string of data to a file and then decoding that string to JSON within the file.

To get around the Foritfy scanning requirements you have to use base64 encoding on the string before sending it through the Ajax call:

var update = $.ajax({
    type: "POST",
    url: "SFPNotesUpdate/../php/validate_notes.php",
    data: { tableData: btoa(JSON.stringify(HotRegisterer.getInstance('myhot').getData())) },
    dataType: 'json'
});

When reading the input to decode the JSON string to an array you have to perform these actions in order:

  • base64_decode the input string
  • sanitize the input string
  • decode the JSON string to an array
$tbl_data = json_decode(filter_var(base64_decode($_POST['tableData']), FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES), true);

PHP Curl and Mangled Headers

I have PHP code that calls out to a SOAP endpoint, gets the response XML, and then does stuff. Well … that was the plan anyway. What actually happened was nothing. No error set, but the response string was null. Even adding curl debugging produced nothing. Fortunately, the endpoint is a sandbox one and thus available on http without encryption. I was going to do a network trace, so I needed to run the script from my computer. Aaaand STDOUT is where the curl debugging was going, not (as I assumed) STDERR. And look, there actually was an error. 400 bad request (how did that not come through to curl_error?!?) — and it became immediately obvious what my problem was — the content length is a quoted string instead of an integer.

Sigh! Copy/paste error where I built out my header. Cleared the escaped quotes from around the strlen call and, voila, my code works perfectly.

But a note to my future self — when PHP’s curl calls aren’t producing any output … try running it from the CLI. Or remember to add the instruction to have verbose output in STDERR!

curl_setopt($ch, CURLOPT_STDERR, $verbose);

 

Fortify on Demand Remediation – XSS Reflected

This vulnerability occurs when you accept user input and then use that input in output. The solution is to sanitize the input. The filter_var or filter_input functions can be used with a variety of sanitize filters.

As an example, code to accept what should be an integer value from user input:

     $iVariable = $_POST['someUserInput'];

Becomes code that removes all characters except for digits (and + or – signs) from the input:

     $iVariable = filter_input(INPUT_POST, 'someUserInput', FILTER_SANITIZE_NUMBER_INT);

Fortify on Demand Remediation – Introduction

The company for which I work signed a contract with some vendor for cloud-based static code analysis. We ran our biggest project through it and saw just shy of ten thousand vulnerabilities. Now … when an application sits out on the Internet, I get that a million people are going to try to exploit whatever they can in order to compromise your site. When the app is only available internally? I fully support firing anyone who plays hacker against their employer’s tools. When a tool is an automation that no one can access outside of the local host? Lazy, insecure code isn’t anywhere near the same problem it is for user-accessible sites. But the policy is the policy, so any code that gets deployed needs to pass the scan — which means no vulnerabilities identified.

Some vulnerabilities have obvious solutions — SQL injection is one. It’s a commonly known problem — a techy joke is that you’ll name your kid “SomeName’;DROP TABLE STUDENTS; … and most database platforms support parameterized statements to mitigate the vulnerability.

Some vulnerabilities are really a “don’t do that!” problem — as an example, we were updating the server and had a page with info(); on it. Don’t do that! I had some error_log lines that output user info that would be called when the process failed (“Failed to add ecckt $iCircuitID to work order $iWorkOrderID for user $strUserID with $curlError from the web server and $curlRepsonse from the web service”). I liked having the log in place so, when a user rang up with a problem, I had the info available to see what went wrong. The expedient thing to do here, though, was just comment those error_log lines out. I can uncomment the line and have the user try it again. Then checkout back to the commented out iteration of the file when we’re done troubleshooting.

Some, though … static code analysis tools don’t always understand that a problem is sorted when the solution doesn’t match one of their list of ‘approved’ methods. I liken this to early MS MCSE tests — there was a pseudo-GUI that asked you to share out a printer from a server. You had to click the exact right series of places in the pseudo-GUI to answer the question correctly. Shortcut keys were not implemented. Command line solutions were wrong.

So I’ve started documenting the solutions we find that pass the Fortify on Demand scan for everything identified in our scans — hopefully letting the next teams that use the static scanner avoid the trial-and-error we’ve gone through to find an acceptable solution.

Oracle – Collections and IN or LIKE Queries

I’ve been retrofitting a lot of PHP/SQL queries to use oci_bind_by_name recently. When using “IN” clauses, you can iterate through your array twice, but it’s an inefficient approach.

// Build the query string with a bunch of placeholders
$strQuery = "select Col1, Col2, Col3 from TableName where ColName IN (";
for($i=0; $i < count($array); $i++){
    if($i > 0){
        $strQuery = $strQuery . ", ";
    }
    $strQuery = $strQuery . ":bindvar" . $i;
}
$strQuery = $strQuery . ")";
... 
// Then bind each placeholder to something
for($i=0; $i < count($array); $i++){
    oci_bind_by_name($stmt, ":bindvar".$i, $array[$i]);
}

Building a table from the array data and using an Oracle collection object creates cleaner code and avoids a second iteration of the array:

$strQuery = "SELECT indexID, objName FROM table WHERE objName in (SELECT column_value FROM table(:myIds))";
$stmt = oci_parse($conn, $strQuery);

$coll = oci_new_collection($kpiprd_conn, 'ODCIVARCHAR2LIST','SYS');
foreach ($arrayValues as $strValue) {
     $coll->append($strValue);
}
oci_bind_by_name($stmt, ':myIds', $coll, -1, OCI_B_NTY);
oci_set_prefetch($stmt, 300);
oci_execute($stmt);

A simple like clause is quite straight-forward

$strNameLikeString = "SomeName%";
$strQuery = "SELECT ds_dvrsty_set_nm from ds_dvrsty_set WHERE ds_dvrsty_set_nm LIKE :divsetnm ORDER BY ds_dvrsty_set_nm DESC fetch first 1 row only";

$stmt = oci_parse($connDB, $strQuery);
oci_bind_by_name($stmt, ":divsetnm", $strNameLikeString);
oci_set_prefetch($stmt, 300);
oci_execute($stmt);

But what about an array of inputs essentially reproducing the LIKE ANY predicate in PostgreSQL? There’s not a direct equivalent in Oracle, and iterating through the array twice to build out a query WHERE (Field1 LIKE ‘Thing1%’ OR Field1 LIKE ‘Thing2%’ OR Field1 LIKE ‘Thing3%’) is undesirable. The with EXISTS allows me to create a LIKE ANY type query and only iterate through my array once to bind variables to placeholders using the same collection approach as was used with the IN clause.

$arrayLocs = array('ERIEPAXE%', 'HNCKOHXA%', 'LTRKARXK%');
$strQuery = "SELECT location_id, clli_code FROM network_location WHERE EXISTS (select 1 FROM TABLE(:likelocs) WHERE clli_code LIKE column_value)";
$stmt = oci_parse($connDB, $strQuery);

$coll = oci_new_collection($connDB, 'ODCIVARCHAR2LIST','SYS');
foreach ($arrayLocs as $strLocation) {
    $coll->append($strLocation);
}
oci_bind_by_name($stmt, ':likelocs', $coll, -1, OCI_B_NTY);
oci_execute($stmt);
print "<table>\n";
print "<tr><th>Loc ID</th><th>CLLI</th></tr>\n";
while ($row = oci_fetch_array($stmt, OCI_ASSOC+OCI_RETURN_NULLS)) {
    print "<tr><td>" . $row['LOCATION_ID'] . "</td><td>" . $row['CLLI_CODE'] . "</td></tr>\n";
}
print "</table>\n";

There are many different collection types in Oracle which can be used with oci_new_collection. A full list of the system collection types can be queried from the database.

SELECT * FROM SYS.ALL_TYPES WHERE TYPECODE = 'COLLECTION' and OWNER = 'SYS';