PHP inet_ntop() is acting differently on real VPS - php

I deployed my application on a VPS and It works fine expect the inet_ntop() function. On my local web server it's working fine with no error message.
I store the IP addresses in a binary(16) column. Before I would store them into the database I convert them. My last login IP is: 4e43b7, in binary numbers: 00110100 01100101 00110100 00110011 01100010 00110111 and the readable format: 78.92.67.183. So it run with a warning message on the VPS: Warning: inet_ntop(): Invalid in_addr value in home/...
What's wrong? According to the PHP doc the function works on PHP 5.1 or higher and a VPS has 5.5, but I am using 5.6. Can version differences cause problem? This line causes the warning:
$ip = inet_ntop($datas['ip_address'];

As per the manual:
When BINARY values are stored, they are right-padded with the pad value to the specified length. The pad value is 0x00 (the zero byte).
so you're not pulling out a 32bit IPv4 address. You're pulling out an 16x8bit = 128bit value, and feeding that to inet_ntop().

As Marc B noted, your BINARY column is being right-padded with zeroes which is then throwing off your decoding. There's not a super good way of handling this, particularly within just MySQL.
However, IPv6 has a built-in method of encapsulating IPv4 addresses, 6to4. Using this you can store v4 and v6 addresses side-by-side in a way that's maintainable and easy to deal with.
function fourToSix($addr) {
$haddr = str_pad(dechex(ip2long($addr)), 8, '0', STR_PAD_LEFT);
$v6 = sprintf('2002:%s::', implode(':', str_split($haddr, 2)));
return $v6;
}
function sixToFour($addr) {
return long2ip(hexdec(implode('', array_slice(explode(':', $addr), 1, 4))));
}
function isSixToFour($addr) {
return explode(':', $addr)[0] == '2002';
}
$v4 = '10.1.2.3';
$v6 = '2002:0a:01:02:03::';
var_dump(
fourToSix($v4),
isSixToFour($v6),
sixToFour($v6),
bin2hex(inet_pton($v6))
);
Output:
string(18) "2002:0a:01:02:03::"
bool(true)
string(8) "10.1.2.3"
string(32) "2002000a000100020003000000000000"
edit
Example store:
$stmt = $dbh->prepare('INSERT INTO table (id, addr) VALUES(?, ?);');
// convert IP if it is v4
if( ip2long($ip_addr) !== false ) {
$ip_addr = fourToSix($ip_addr);
}
$stmt->execute(1, inet_pton($ip_addr));
Example retrieve:
$addr_raw = $dbh->query('SELECT addr FROM table WHERE id=1')->fetch(PDO::FETCH_ASSOC)['addr'];
$addr_str = inet_ntop($addr_raw);
if( isSixToFour($addr_str) ) {
printf("IPv4 address is: %s\n", sixToFour($addr_str));
} else {
printf("IPv6 address is: %s\n", $addr_str);
}

Related

imap_mail_move() not working on special characters (äüö...)

I am using imap_mail_move() to move emails from one folder to another. This works pretty well, but not if it comes to special characters in the folder name. I am sure I need to encode the name, but all test where not succesful.
Anybody that has a nice idea? Thanks in advance.
class EmailReader {
[...]
function doMoveEmail($uid, $targetFolder) {
$targetFolder = imap_utf8_to_mutf7($targetFolder);
$return = imap_mail_move($this->conn, $uid, $targetFolder, CP_UID);
if (!$return) {
$this->printValue(imap_errors());
die("stop");
}
return $return;
}
[...]
}
Calling the function in the script
[...]
$uid = 1234;
$folderTarget1 = "INBOX.00_Korrespondenz";
$this->doMoveEmail($uid, $folderTarget1);
$folderTarget2 = "INBOX.01_Anmeldevorgang.011_Bestätigungslink";
$this->doMoveEmail($uid, $folderTarget2);
[...]
The execution of the first call (folderTarget1) is working pretty well.
The execution of the secound call (folderTarget2) is creating an error:
[TRYCREATE] Mailbox doesn't exist: INBOX.01_Anmeldevorgang.011_Bestätigungslink (0.001 + 0.000 secs).
Remark 1:
if I call imap_list(), the name of the folder is shown as
"INBOX.01_Anmeldevorgang.011_Besta&Awg-tigungslink" (=$val)
using:
$new = mb_convert_encoding($val,'UTF-8','UTF7-IMAP')
echo $new; // gives --> "INBOX.01_Anmeldevorgang.011_Bestätigungslink"
but:
$new2 = mb_convert_encoding($new,'UTF7-IMAP', 'UTF-8')
echo $new2; // gives --> "INBOX.01_Anmeldevorgang.011_Best&AOQ-tigungslink"
Remark 2
I checked each possible encoding, with the following script, but none of them matchs the value that is returned by imap_list().
// looking for "INBOX.01_Anmeldevorgang.011_Besta&Awg-tigungslink" given by imap_list().
$targetFolder = "INBOX.01_Anmeldevorgang.011_Bestätigungslink";
foreach(mb_list_encodings() as $chr){
echo mb_convert_encoding($targetFolder, $chr, 'UTF-8')." : ".$chr."<br>";
}
Your folder name, as on the server, Besta&Awg-tigungslink is not canonically encoded:
&Awg- decodes as the combining diaereses character. Using some convenient python to look it up:
import base64
import unicode data
x = base64.b64decode('Awg=').decode('utf-16be'); # equals added to satisfy base64 padding requirements
unicodedata.name(x)
# Returns 'COMBINING DIAERESIS'
This combines with the a in front of it to show ä.
Your encoder is returning the more common precomposed form:
x = base64.b64decode('AOQ=').decode('utf-16be')
unicodedata.name(x)
# Returns: 'LATIN SMALL LETTER A WITH DIAERESIS'
This is a representation of ä directly.
Normally, when you work with IMAP folders, you pass around the raw name, and only convert the folder name for display. As you can see, there is not necessarily a one-way mapping from glyphs to encodings in unicode.
It does surprise me that PHP does seem to be doing a canonicalization step when encoding; I would expect round tripping the same data to return the same thing.
I created a workaround, which helps me to work with UTF8-values and to translate it to the original (raw) IMAP folder name.
function getFolderList() {
$folders = imap_list($this->conn, "{".$this->server."}", "*");
if (is_array($folders)) {
// Remove Server details of each element of array
$folders = array_map(function($val) { return str_replace("{".$this->server."}","",$val); }, $folders);
// Sort array
asort($folders);
// Renumber the list
$folders = array_values($folders);
// add UTF-8 encoded value to array
// this is needed as the original value is so wiered, that it is not possible to encode it
// with a function on the fly. This additional utf-8 value is needed to map the utf-8 value
// to the original value. The original value is still needed to do some operations like e.g.:
// - imap_mail_move()
// - imap_reopen()
// ==> the trick is to use normalizer_normalize()
$return = array();
foreach ($folders as $key => $folder) {
$return[$key]['original'] = $folder;
$return[$key]['utf8'] = normalizer_normalize(mb_convert_encoding($folder,'UTF-8','UTF7-IMAP'));
}
return $return;
} else {
die("IMAP_Folder-List failed: " . imap_last_error() . "\n");
}
}

Validate RRSIG with PHP using openssl

I'm trying to do a RRSIG validation, I'm trying to use the openssl lib in PHP. But I'm having a problem to pass the public key to the openssl_verify function.
This is a base code,
using the Net/DNS2 library to do a DNS query with DNSSEC option.
and get the DNSKEY and RRSIG.
<?php
require_once 'Net/DNS2.php';
$r = new Net_DNS2_Resolver(array('nameservers' => array('127.0.0.1')));
$r->dnssec = true;
try {
$result = $r->query('ip4afrika.nl', 'DNSKEY');
} catch(Net_DNS2_Exception $e) {
echo "::query() failed: ", $e->getMessage(), "\n";
die(); //
}
// print_r($result->answer);
$public_key_bin = base64_decode( $result->answer[0]->key ) ;
$public_key_str = $result->answer[0]->key; //echo $public_key_str; die();
// $public_key_res = openssl_x509_parse($public_key_bin);
$public_key_res = openssl_x509_read($public_key_str);
// $public_key_res = openssl_pkey_get_public($public_key_str);
while ($msg = openssl_error_string()) echo $msg . PHP_EOL;
I get this error messages,
when using:
$public_key_res = openssl_x509_read($public_key_str);
PHP Warning: openssl_x509_read(): supplied parameter cannot be
coerced into an X509 certificate! in /src/Net_DNS2-1.4.3/i.php on line
34 PHP Stack trace: PHP 1. {main}() /src/Net_DNS2-1.4.3/i.php:0 PHP
2. openssl_x509_read() /src/Net_DNS2-1.4.3/i.php:34 error:0906D06C:PEM routines:PEM_read_bio:no start line
so i tried adding the BEGIN/END headers
$public_key_str = '-----BEGIN CERTIFICATE-----' . PHP_EOL . $result->answer[0]->key . PHP_EOL . '-----END CERTIFICATE-----' ;
And got this error messages,
error:0D0680A8:asn1 encoding routines:ASN1_CHECK_TLEN:wrong tag
error:0D07803A:asn1 encoding routines:ASN1_ITEM_EX_D2I:nested asn1 error
error:0906700D:PEM routines:PEM_ASN1_read_bio:ASN1 lib
So it seems I'm feeding the function the wrong format, I'm still googling but any help would be welcome.
Eventually I like to validate the signature with:
openssl_verify($data, $signature, $public_key_res, 'RSA-SHA256');
Short answer:
If you just need the capability in PHP, you can just use https://github.com/metaregistrar/php-dnssec-validator .
Long Answer:
The reason you can't load the KEY data is because it's in a slightly different format. According to rfc3110:
Field Size
----- ----
exponent length 1 or 3 octets (see text)
exponent as specified by length field
modulus remaining space
Whereas RSA public keys are a bit more complex -- aside from the exponent and modulus, you need to prefix it with the correct OID as such (2nd answer).
After that the process is a bit gnarly:
Get the RRSIG record to get the signature and key tag (to determine which key to use)
Use the public keys from the correct DNSKEY RR to verify the signature against.
There's also a process described here (in python)
PHP_EOL is platform specific, not sure what platform you're testing this code on but try replacing the constant with '\n' explicitly. See if that helps.

InApp Billing Verifying Order on Web Server PHP

I'm using a simple PHP script to verify Android order to parse download for the customer.
$receipt = $_GET['purchaseData'];
$billInfo = json_decode($receipt,true);
$signature = $_GET['dataSignature'];
$public_key_base64 = "xxxxxxxxxxxxxxxx";
$key = "-----BEGIN PUBLIC KEY-----\n".
chunk_split($public_key_base64, 64,"\n").
'-----END PUBLIC KEY-----';
$key = openssl_get_publickey($key);
$signature = base64_decode($signature);
//$result = openssl_verify($billInfo, $signature, $key);
$result = openssl_verify($receipt, $signature, $key);
if (0 === $result) {
echo "0";
} else if (1 !== $result) {
echo "1";
} else {
echo "Hello World!";
}
//added the var_dump($result); as asked by A-2-A
var_dump($result);
result is 0int(0)
I made a real order through the App after I published it and when trying to validate the order I get "0" as result.
I tried direct HTTP access
https://domain.com/thankyou.php?purchaseData={"packageName":"com.example.app","orderId":"GPA.1234-5678-1234-98608","productId":"product","developerPayload":"mypurchasetoken","purchaseTime":1455346586453,"purchaseState":0,"developerPayload":"mypurchasetoken","purchaseToken":"ggedobflmccnemedgplmodhp...."}&dataSignature=gwmBf...
I'm keeping the first of the question because my result is still a guess. After further investigation I think it's the signature not being read in a nice clean way as sent by google.
The signature=gwmBfgGudpG5iPp3L0OnepNlx while the browser is reading it as ƒ ~®v‘¹ˆúw
How is it possible to let it be read in the right way?
To verify the signature you want to make sure of the following:
INAPP_PURCHASE_DATA is not mutated in any way. Any encoding or escaping changes will result in a invalid verification. The best way to ensure it gets to your server intact is to base64 encoded it.
INAPP_DATA_SIGNATURE also must remain intact, it should already base64 encoded so sending that to your server should not be a problem.
openssl_verify expects both data and signature arguments to be in their raw state, so base64 decode before verifying.
It also takes signature_alg as the last argument, in this case sha1WithRSAEncryption should work as should the default, but if in doubt try a few other sha1 algorithms to see which ones work.
My best guess why it's not working for you right now is that you're not receiving the INAPP_PURCHASE_DATA on your server in the same condition that it was received on the app. This Stackoverflow question had the same problem.

json_decode Preservation of Type

I'm using the json_decode function to decode (and verify a postback from a payment processor). the json object received looks as follow
{
"notification":{
"version":6.0,
"attemptCount":0,
"role":"VENDOR",
.....
"lineItems":[
{
"itemNo":"1",
"productTitle":"A passed in title",
"shippable":false,
"recurring":false,
"customerProductAmount":1.00,
"customerTaxAmount":0.00
}
]
},
"verification":"9F6E504D"
}
The verification works as follows, one takes the notification node and append a secret key. The first eight characters of the SHA1 hash of this string should match the content of the validation node.
However, I noticed that whilst using json_decode, the double value 6.0, 0.00 etc are truncated to integers (6, 0 ,etc). This messes up the string (in terms of it not generating the correct SHA1-hash). Do note, I cannot use the depth limit to prevent decoding of the notification branch, since I need to support PHP 5.0. How can I tackle this issue. The (defect) validation code I wrote is:
public function IPN_Check(){
$o = (json_decode($this->test_ipn));
$validation_string = json_encode($o->notification);
}
I tried the following:
<?php
var_dump(json_decode('
{
"notification":{
"version":6.0,
"attemptCount":0
}
}
'));
and got this output:
object(stdClass)#1 (1) {
["notification"]=>
object(stdClass)#2 (2) {
["version"]=>
float(6)
["attemptCount"]=>
int(0)
}
}
PHP does make a difference between float and int, maybe you could do something like gettype($o->notification[$i]) == 'float' to check whether you need to add a zero using a string.
UPD.
PHP does make a difference between float and int, but json_encode() - may not. To be sure, that you encode all values as they are - use json_encode() with JSON_PRESERVE_ZERO_FRACTION parameter, and all your float/double types will be saved correctly.
It looks like ClickBank they always send it in the same format with only the two top level fields "notification" and "verification". So you can just use substr to remove the first 16 characters ({"notification":) and the last 27 characters (,"verification":"XXXXXXXX"}) from the raw JSON and then proceed from there:
$notification = substr($json, 16, -27);
$verification = strtoupper( substr( hash('sha1', $notification . $secret), 0, 8) );

Validating user IP and form IP: Should I use ip2long?

Actually this is a two parts question:
First, is ip2long a good IP validator? Suppose someone inserts an IP in a form, and I want to validate that it is correct. Is it OK to just check that ip2long doesn't return FALSE ?
Second: What do you think about checking the IP of the visitors, and denying access if it's not a valid IP? Looking at my visitor's ips, sometimes I find things like "1.1 TIBURON".. Wtf is that? I heard the expression 'spoofed ip', is that what it is? Is it related to spam bots?
If you just need to valdiate that the IP address is correct in format you can use a regular expresión like the one bellow.
EDIT:
if (preg_match('/\A(?:^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)\Z/im', $subject)) {
# Successful match
} else {
# Match attempt failed
}
If you want to go further you can do a ping to the IP to discover if it is active.
About your second question, i don't know what to say, i've never seen the "1.1 TIBURON" thing,
HTH
Depends on how thorough you want the validation to be. A very simple check for well-formed IP addresses would be a regex along the lines of /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/. (Actually, SubniC's regex is probably much better). This would catch your 1.1. TIBURON entry, because it's not well-formed.
I'm not sure whether ip2long checks this or just silently discards the part of the string that doesn't look like an IP.
One step further you could block 'reserved' IP addresses and ranges, such as 127.0.0.1, 255.255.255.255, etc.
You might also want to add a blacklist to filter out requests from dubious networks, or from clients you have had trouble with in the past.
I'm curious though how that entry got there in the first place - an attacker shouldn't be able to get this value into $_SERVER['REMOTE_ADDR'] unless they have compromised the underlying OS, or at least apache (or whatever web server you're running).
You're not by any chance reading the IP on the client (using javascript)?
I had this code around. It's been a while and I can't quite remember some of the decisions behind it, but I assure you it has been thoroughly tested
(actually I just tweaked it a little, renaming variables and adding comments, so it's possible I just broke it :)
You may find the get_ip_addr and is_private_ip_addr functions useful.
get_ip_addr can take both an IP address or a host name.
<?php
function get_ip_addr($host){
$long_ipaddr = my_ip2long($host);
$host = trim($host);
if( $long_ipaddr !== false ){
$str_ip = long2ip($long_ipaddr); // Because of e.g. 245.254.245
return $str_ip;
}
$ip_addr = #gethostbyname($host);
if( $ip_addr and $ip_addr != -1 ){
return $ip_addr;
}
return false;
}
function my_ip2long($ipaddr){
$long_ip = ip2long($ipaddr);
if( $long_ip === false ){
return false;
}
// http://php.net/manual/en/function.ip2long.php says:
// Note:
// Because PHP's integer type is signed, and many IP addresses
// will result in negative integers, you need to use
// the "%u" formatter of sprintf() or printf() to get
// the string representation of the unsigned IP address.
$long_ip = sprintf("%u", $long_ip);
return $long_ip;
}
function ip2bin($ip){
$octets = explode(".", $ip);
foreach($octets as $k => $v){
$octets[$k] = str_pad(decbin($v), 8, "0", STR_PAD_LEFT);
}
return implode('', $octets);
}
function ip_in_range($ip, $prefix, $mask_len){
$ip = ip2bin($ip);
$prefix = ip2bin($prefix);
$ip = substr($ip, 0, $mask_len);
$prefix = substr($prefix, 0, $mask_len);
// Watch out! Two numerical strings are converted to integers
// when you use ==. This is trouble for long integers.
// Using === skips this behaviour
return ($ip === $prefix);
}
function is_private_ip_addr($ipaddr){
if( "localhost" === $ipaddr ) return true;
$long_ipaddr = my_ip2long($ipaddr);
if( $long_ipaddr === false ){
return false; // Shouldn't be calling this!
}
// Info below obtained from http://bugs.php.net/bug.php?id=47435#c145655
// Not sure why 127.0.0.0/8 isn't mentioned ...?
// Also, in IPv6 there's the multicast address range: ff00::/8s
//
// IPv4 private ranges
// 10.0.0.0/8 // private use network (rfc1918)
// 172.16.0.0/12 // private use network (rfc1918)
// 192.168.0.0/16 // private use network (rfc1918)
//
// IPv4 reserved ranges
// 0.0.0.0/8 // "this" network (rfc1700)
// 169.254.0.0/16 // link local network (rfc3927)
// 192.0.2.0/24 // test net (rfc3330)
// 224.0.0.0/4 // Multicast (rfc3171)
// 240.0.0.0/4 // Reserved for Future Use (rfc1700)
//
// IPv6 Private range
// fc00::/7 // unique-local addresses (rfc4193)
//
// IPv6 Reserved ranges
// ::/128 // unspecified address (rfc4291)
// ::1/128 // loopback address (rfc4291)
// fe80::/10 // link local unicast (rfc4291)
// 2001:db8::/32 // documentation addresses (rfc3849)
// 5f00::/8 // 6Bone
// 3ffe::/16 // 6Bone
// ::ffff:0:0/96 // IPv4-Mapped addresses (rfc4291)
// 2001:10::/28 // ORCHID addresses (rfc4843)
// ::/0 // default unicast route address
//
// Anyways, this are the relevant RFCs:
// RFC 3330 (Sep 2002), "Special-Use IPv4 Addresses":
// see section 3 for a nice table
// RFC 5156 (Apr 2008), "Special-Use IPv6 Addresses"
//
// Also, this function currently only deals with IPv4 addresses,
// since PHP's ip2long simply doesn't support IPv6 anyway.
// You can't currently even trust long ints to have 64 bit,
// so a 128 bit long int is out of the question.
if( ip_in_range($ipaddr, "127.0.0.0", 8) ) return true; // Loopback
if( ip_in_range($ipaddr, "10.0.0.0", 8) ) return true; // Private addresses (Class A range)
if( ip_in_range($ipaddr, "172.16.0.0", 12) ) return true; // Private addresses (Class B range)
if( ip_in_range($ipaddr, "192.168.0.0", 16) ) return true; // Private addresses (Class C range)
if( ip_in_range($ipaddr, "169.254.0.0", 16) ) return true; // "This" network
if( ip_in_range($ipaddr, "192.0.2.0", 24) ) return true; // "TEST-NET" (documentation and examples)
if( ip_in_range($ipaddr, "224.0.0.0", 4) ) return true; // Multicast
if( ip_in_range($ipaddr, "240.0.2.0", 4) ) return true; // Reserved for future use
return false;
}
You can consider this code released under a permissive CC BY-SA.
Do whatever you want with it, but if you improve on it please tells us about it here.
I'm marking this post community wiki..

Categories