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

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..

Related

PHP - How to use config file variables in a function's array?

I could not find an example of what I'm trying to accomplish. I guess I don't know the proper search words to use. I have a working script but I want to make it more flexible by adding a main, admin editable config file.
I have the following function:
function ip_is_mobile($ip) {
$pri_addrs = array(
'66.87.0.0-66.87.255.255', // Sprint mobile
'174.192.0.0-174.255.255.255' // Verizon mobile
);
$long_ip = ip2long($ip);
if($long_ip != -1) {
foreach($pri_addrs AS $pri_addr) {
list($start, $end) = explode('-', $pri_addr);
// IF IS a mobile IP
if($long_ip >= ip2long($start) && $long_ip <= ip2long($end))
return true;
}
}
return false;
}
I would like to replace the hard-coded IP address ranges, in the function, with variables or definitions which will be set in the main config file so that the config file has something similar to the following:
// Mobile IP address ranges. Add as many as needed.
$MobileIPs['0']="66.87.0.0-66.87.255.255";
$MobileIPs['1']="174.192.0.0-174.255.255.255";
$MobileIPs['2']="85.110.50.0/24";
My goal is to give the admin an easy to read and understand way of adding as many IP address ranges as necessary (probably 20 max). I'm not opposed to totally rewriting the function if there is a better, more efficient way. In addition to IP ranges, it would be advantageous if CIDR's could also be specified; as indicated in the last code line above.
What edits do I need to make to the function and what would the corresponding lines in the main config file be so that the user can add any number of ranges or CIDR's?
You can store configuration of ip ranges in separate configuration file and use require_once in your main code
ip_ranges.conf.php (configuration file)
<?php
$pri_addrs = array(
'66.87.0.0-66.87.255.255', // Sprint mobile
'174.192.0.0-174.255.255.255' // Verizon mobile
);
index.php (main code file)
function ip_is_mobile($ip) {
require_once(ip_ranges.conf.php); // include config file
$long_ip = ip2long($ip);
if($long_ip != -1) {
foreach($pri_addrs AS $pri_addr) {
list($start, $end) = explode('-', $pri_addr);
// IF IS a mobile IP
if($long_ip >= ip2long($start) && $long_ip <= ip2long($end))
return true;
}
}
return false;
}

How to block certain Ip ranges via ip2long in php

Lets assume I have the following IP ranges that I want to block
89.96.53.158 and 89.96.53.189
This how am trying to implement it.
My questions: is it the best way to do it as per code below. Is the coding below right for it
$ip = sprintf('%u', ip2long($_SERVER['REMOTE_ADDR']));
$start_ip = sprintf('%u', ip2long("89.96.53.158"));
$end_ip = sprintf('%u', ip2long("89.96.53.189"));
// stop only ip range between 89.96.53.158 - 89.96.53.189
if ($ip >= $start_ip && $ip <= $end_ip) {
echo "you cannot access our site";
exit();
}
can I also achieve that using strpos() functions
Yes I figured out another way to do it with strpos() method
if(strpos($_SERVER['REMOTE_ADDR'], "89.96") === 0)
{
echo "you cannot access our site";
exit();
}
If you notice, the === operator makes sure that the 89.96 is at the beginning of the IP address.
This means that you can specify as much of the IP address as you want, and it will block no matter what numbers come after it.

How to detect whether a URL is local area network(LAN) url by PHP? [duplicate]

I need to check if a file is opened "locally" (same machine or network). I'm using:
<?php
if ((substr($_SERVER['REMOTE_ADDR'],0,8) == "192.168.") || ($_SERVER['REMOTE_ADDR'] == "127.0.0.1")) {
// client is local
} else {
// client is not local
}
But I'm not sure this is the best way.
What is a more foolproof way of doing this?
What Friek said is true, but provided that you know how to get the real client's IP, you can tell if it's a local address using PHP filters:
if ( ! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) )
{
// is a local ip address
}
"Foolproof," as always, can be tricky.
If we do restrict ourselves to IPv4, then checking for "127.0.0.1" takes care of the localhost case, but checking against "192.168." is plain wrong - it will only work if the script is being run on a server which happens to be on the 192.168 network, using a 16-bit subnet mask.
Checking $_SERVER['REMOTE_ADDR'] against $_SERVER['SERVER_ADDR'] would be a better bet. This still doesn't take care of the case of a multi-homed host (ie one which has several IP addresses in addition to 127.0.0.1), though.
In order to catch all same-network cases, you'd need to check the combination of SERVER_ADDR and subnet mask against REMOTE_ADDR, but the subnet mask isn't available in $_SERVER.
BUT I found a function which does pretty much what you want here. It's a couple of screens down and it's called clientInSameSubnet. Not my code, but looks right.
In case anyone has trouble finding the above code, suggested by #Uffe, I've included it below:
<?php
/**
* Check if a client IP is in our Server subnet
*
* #param string $client_ip
* #param string $server_ip
* #return boolean
*/
function clientInSameSubnet($client_ip=false,$server_ip=false) {
if (!$client_ip)
$client_ip = $_SERVER['REMOTE_ADDR'];
if (!$server_ip)
$server_ip = $_SERVER['SERVER_ADDR'];
// Extract broadcast and netmask from ifconfig
if (!($p = popen("ifconfig","r"))) return false;
$out = "";
while(!feof($p))
$out .= fread($p,1024);
fclose($p);
// This is to avoid wrapping.
$match = "/^.*".$server_ip;
$match .= ".*Bcast:(\d{1,3}\.\d{1,3}i\.\d{1,3}\.\d{1,3}).*";
$match .= "Mask:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/im";
if (!preg_match($match,$out,$regs))
return false;
$bcast = ip2long($regs[1]);
$smask = ip2long($regs[2]);
$ipadr = ip2long($client_ip);
$nmask = $bcast & $smask;
return (($ipadr & $smask) == ($nmask & $smask));
}

PHP inet_ntop() is acting differently on real VPS

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);
}

How to strip subdomains to get valid root domain using PHP?

Ok, here's what I'm looking for: from a list of links, I'm stripping everything but the domains. The result is a mixed list of domains and domain-names which represent subdomains.
stackoverflow.com
security.stackexchange.com
whoknows.test.co.uk
you.get.it.by.now.dont.you.com
What I want to do is to trim all list entries down to their VALID (=only existing) root domains like this:
stackoverflow.com
security.stackexchange.com
test.co.uk
-fail-
Currently I explode each line into an array and work my list from back to front, using curl to check each potential root domain for it's existance... as soon as curl throws back a HTTP code >= 200 and < 400, I regard the root domain to be found. When the end of each potential domain lookup is done and no valid domain has been found at all, the domain is considered to be non-existant.
input: stackoverflow.com
test: stackoverflow.com - succeeds and is the root domain
result: stackoverflow.com - valid root domain
input: whoknows.test.co.uk
test: co.uk - fails
test: test.co.uk - succeeds (theoretically) and is the root domain
result: test.co.uk - valid root domain
input: you.get.it.by.now.dont.you.com
test: you.com - fails
test: dont.you.com - fails
test: now.dont.you.com - fails
test: by.now.dont.you.com - fails
test: it.by.now.dont.you.com - fails
test: get.it.by.now.dont.you.com - fails
test: you.get.it.by.now.dont.you.com - fails
result: you.get.it.by.now.dont.you.com - invalid domain
Is there any alternative way to do this? I would like to stop heating up my webserver's CPU with 2 to X (=near to unlimited) curl look-ups for every domain on my 100.000+ list. Also, all these lookups take a bunch of time. Maybe - so I hope - there is a quicker solution to do this.
The catch? It has to work with PHP.
There are a bunch of shortcuts to acheive what you need.
For example, you already know that .co.uk and .com are TLDs, so checking these you can obviously skip.
The problem is with all the other crazy TLDs.
I suggest you take a look at the source for ruby-domain-name
They have done a lot of work using RFCs and known data, to try and process it the right way.
So...
I've been fiddling around with this for a long time now, looking for the potential bottlenecks and after a few days of back and forth I discovered that it's actually CURL (that's waiting for the individual servers to respond with a HTTP code) that's making things slower than needed.
In the end, I opted in for a different "gethostbyname" function that takes IP6 into account to solve my problem(s).
function my_gethostbyname($host, $try_a = FALSE)
{
$dns = gethostbynamel6($host, $try_a);
if ($dns == FALSE)
{
return FALSE;
}
else
{
return $dns[0];
}
}
function gethostbynamel6($host, $try_a = FALSE)
{
$dns = array();
$dns6 = #dns_get_record($host, DNS_AAAA);
if($dns6!== FALSE)
{
$dns = array_merge($dns, $dns6);
}
if ($try_a == TRUE)
{
$dns4 = #dns_get_record($host, DNS_A);
if($dns4!== FALSE)
{
$dns = array_merge($dns, $dns4);
}
}
else
{
$dns = $dns6;
}
$ip6 = array();
$ip4 = array();
foreach ($dns as $record)
{
if ($record["type"] == "A")
{
$ip4[] = $record["ip"];
}
if ($record["type"] == "AAAA")
{
$ip6[] = $record["ipv6"];
}
}
if (count($ip6) < 1)
{
if ($try_a == TRUE)
{
if (count($ip4) < 1)
{
return FALSE;
}
else
{
return $ip4;
}
}
else
{
return FALSE;
}
}
else
{
return $ip6;
}
}
As soon as the first domain-part actually resolves to an IP, (a) the domain exists and (b) is the root domain.
This spares me major time and the trick of this is that you're only as slow as your DNS resolution and some microsecs. The curl option I used before took around 3 seconds per call (sometimes up to the full timeout range I had set to 20 secs), depending on the target server's response time - if any.
The path I now chose is easy to understand: I end up with a list of resolving domains and - when needed - I can check those using curl "on demand" or using one or more CRON jobs "on interval".
I know that it's kind of a workaround, but splitting the problem into two tasks (1 = pre-check for root domain, 2 = check if domain returns expected HTTP code) makes the whole thing faster than trying to do the complete job at once using curl.
What I've learned from this...
When checking domains, try to resolve them first so you can spare yourself the timeout burden that comes with curl.
Curl is great for many tasks, but it's not the smartest way to try to do everything with it.
When you think you can't solve a problem more than you've tried to do, split the problem in two or more parts and check again. Chances are big that you'll discover a whole new world of options to enhance what you've got.
I hope this spares someone the burden of fiddling around with an alike problem for weeks. ;)
class DomainUtils {
function __construct(){
//only these super domains
$this->superDomains = array(
'.com',
'.gov',
'.org',
'.co.uk',
'.info',
'.co',
'.net',
'.me',
'.tv',
'.mobi'
);
}
//get super domain
public function getMainDomain($domain = null){
$domainChunks = explode('.', str_ireplace($this->superDomains, '', $domain));
if(sizeof($domainChunks) == 0){
return false;
}
foreach($domainChunks as $key => $domainChunk){
if($key < sizeof($domainChunks) - 1){
$domain = str_ireplace($domainChunk . '.', '', $domain);
}
}
return $domain;
}
}

Categories