About the security issues of [HTTP_X_FORWARDED_FOR], should I use it at all instead of [REMOTE_ADDR]? - php

I'm trying to create a php counter, and in order not to count repeated visits from the same visitor, I've been thinking about saving the visitor's IP address into the database, and I should turn to $_SERVER
I've read this sample funtion by #Dusza that seems nice and convenient:
<?php
function get_IP() {
// ADDRESS IP
if (getenv('HTTP_CLIENT_IP')) $ipaddress = getenv('HTTP_CLIENT_IP');
else if(getenv('HTTP_X_FORWARDED_FOR')) $ipaddress = getenv('HTTP_X_FORWARDED_FOR');
else if(getenv('HTTP_X_FORWARDED')) $ipaddress = getenv('HTTP_X_FORWARDED');
else if(getenv('HTTP_FORWARDED_FOR')) $ipaddress = getenv('HTTP_FORWARDED_FOR');
else if(getenv('HTTP_FORWARDED')) $ipaddress = getenv('HTTP_FORWARDED');
else if(getenv('REMOTE_ADDR')) $ipaddress = getenv('REMOTE_ADDR');
else $ipaddress = 'UNKNOWN';
//return $ipaddress;
}
?>
But I've done some research here, and found that there's a security hole in that because the user can spoof all values except REMOTE_ADDR, which can be modified by a proxy.
So I guess that when they say that there's a security hole, it means that I should sanitize the user's input when I insert it into the database doing some bindings.
Is there any other precaution?
Given that all other values are unreliable I should avoid using them altogether?
But what about the un-spoffing value of REMOTE_ADDR? That can be modified by a proxy.
Any suggestions on what path should I take?
If you want to downvote, or vote the question to be closed or deleted, please leave
me a comment about why, so I can improve my questions. Thanks.

REMOTE_ADDR is the IP address established through a 3-way TCP/IP handshake. It is the IP the response will be sent back to. It is the only thing that your server has verified. Everything else is just arbitrary HTTP headers anyone could set.
Now, if you know that your server is running behind a proxy (e.g. a load balancer) which would mask the visitor's IP address (your server would only see the proxy's IP), but you know that the proxy is helpfully forwarding you the visitor's IP in an HTTP header (as workaround for this situation so your server can still see the visitor's IP), then and only then may you use one of these HTTP headers and only the one that you know your proxy is setting. If your server is not behind a proxy, use REMOTE_ADDR exclusively. Otherwise, consult your proxy's manual and implement according to the situation.

Related

What if the $_SERVER does not contain the IPv4 ip?

I am trying to code a PHP script for getting the client's IP address using this function:
public function getIpAddress() {
$ipAddress = '';
if (! empty($_SERVER['HTTP_CLIENT_IP']) && $this->isValidIpAddress($_SERVER['HTTP_CLIENT_IP'])) {
// check for shared ISP IP
$ipAddress = $_SERVER['HTTP_CLIENT_IP'];
} else if (! empty($_SERVER['HTTP_X_FORWARDED_FOR']) && $this->isValidIpAddress($_SERVER['HTTP_CLIENT_IP'])) {
// check for IPs passing through proxy servers
// check if multiple IP addresses are set and take the first one
$ipAddressList = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
foreach ($ipAddressList as $ip) {
if ($this->isValidIpAddress($ip)) {
$ipAddress = $ip;
break;
}
}
} else if (! empty($_SERVER['HTTP_X_FORWARDED']) && $this->isValidIpAddress($_SERVER['HTTP_X_FORWARDED'])) {
$ipAddress = $_SERVER['HTTP_X_FORWARDED'];
} else if (! empty($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']) && $this->isValidIpAddress($_SERVER['HTTP_X_CLUSTER_CLIENT_IP'])) {
$ipAddress = $_SERVER['HTTP_X_CLUSTER_CLIENT_IP'];
} else if (! empty($_SERVER['HTTP_FORWARDED_FOR']) && $this->isValidIpAddress($_SERVER['HTTP_FORWARDED_FOR'])) {
$ipAddress = $_SERVER['HTTP_FORWARDED_FOR'];
} else if (! empty($_SERVER['HTTP_FORWARDED']) && $this->isValidIpAddress($_SERVER['HTTP_FORWARDED'])) {
$ipAddress = $_SERVER['HTTP_FORWARDED'];
} else if (! empty($_SERVER['REMOTE_ADDR']) && $this->isValidIpAddress($_SERVER['REMOTE_ADDR'])) {
$ipAddress = $_SERVER['REMOTE_ADDR'];
}else{
$ipAddress = 'error';
}
return $ipAddress;
}
I tried it many times and it's working. But I tried to connect to a VPN and it retrieved IPv6 instead of IPv4!
This could be a normal situation but I need to get the IPv4 of the visitor of that PHP script.
I googled a lot about converting IPv6 to IPv4 and I know that it cannot be converted.
But I notices some IP geolocation services are retrieving IPv4 for the same VPN (on the same device)! For example: canyouseeme.org and api.ipify.org.
That's mean that even if the IPv6 cannot be converted into IPv4, there's a method can be implemented to get IPv4 even if the visitor is using IPv6!
My questions:
If the conversion is not possible between these two versions, how did these IP geolocation services retrieved the IPv4 of the visitor?
If they are not converting the IPv6 into IPv4. So, they are retrieving the IPv4 directly without touching the IPv6. But, how did they do that if the $_SERVER does not contain an IPv4?
In additional to that, I noticed that when I visited whatismyipaddress.com, they retrieved the IPv6 first then it start loading beside the IPv4 field then they retrieved it!
Note: All these sites has retrieved the same IPv4.
Thanks.
Just drop the AAAA record from your DNS name (or disable ipv6 in your webservrr - this might introduce delays as browsers are supposed to prefer ipv6), then, however, people w/o ipv4 cannot access your service any more. As #MagnusEriksson said: "Instead of fighting the future, wouldn't it be better to fix your application so it works with IPv6 as well?"
Also, please be aware, if the ip address is so important for you, that http headers can be easily spoofed by clients (i.e., don't trust user provided data) or proxy servers might provide private ips as defined in RFC1918. You should only consider/use the headers you know a trusted proxy infront of your server uses - in all other cases you cannot trust the user provided headers.
if you are using apache, you may want to check if there are any IPv6 Bindings or VirtualHosts. there will be similar settings for other web servers
if you don't have access to the web server config, you can see if there's an option to disable IPv6 in your webhost panel
also, this could be client-controlled, i.e. when you connect using a VPN then IPv6 is enabled for that network interface (or tunnel). in which case you cannot force the visitor to disable IPv6 on their device / router, but you could try to disable it on your end. unlike HTTPS Everywhere which only accepts port 443 or nothing, I don't think IP addresses are at the stage where it is only possible to accept IPv6 with no fallback to IPv4, so this recommendation should be safe

IP addresses in PHP

I have a competition scripted in PHP, vote based. From one IP, one person can vote for someone one time. I have a log of all IP addresses that voted, but I see something strange. Some IP addresses appear to be like for="ip_address:port", while others are just ip_address, and I see that one ip address, formatted with for="..." appear to be there multiple times, just with different ports. Can someone please explain it to me? How users do this, should I ban them from competition for this?
I use this function to get user IP address:
function get_client_ip_env() {
if (getenv('HTTP_CLIENT_IP'))
$ipaddress = getenv('HTTP_CLIENT_IP');
else if(getenv('HTTP_X_FORWARDED_FOR'))
$ipaddress = getenv('HTTP_X_FORWARDED_FOR');
else if(getenv('HTTP_X_FORWARDED'))
$ipaddress = getenv('HTTP_X_FORWARDED');
else if(getenv('HTTP_FORWARDED_FOR'))
$ipaddress = getenv('HTTP_FORWARDED_FOR');
else if(getenv('HTTP_FORWARDED'))
$ipaddress = getenv('HTTP_FORWARDED');
else if(getenv('REMOTE_ADDR'))
$ipaddress = getenv('REMOTE_ADDR');
else
$ipaddress = 'UNKNOWN';
return $ipaddress;
}
Also, if anyone know some good reference and manual with all predefined variables in PHP, please share it with me.
IP addresses HTTP headers can easily be spoofed and a lot of users (mainly mobile users on for example a wifi connection) have lease times on IP addresses that are very short, thus enabling them to vote again.
That said you can combine options, for example check IP address and set a cookie to make it harder to get around.
If you set a port with the ip address then it will not match your database records/list of IP addresses. Should you ban them for that? I can't answer that.
Most of your checking method is based on what the request says is the IP address, like the HTTP headers, which are easily spoofed. Don't trust them or accept that your poll is not going to get accurate results.
If you really want a fair voting system that allows one vote per person you will need to use something else then IP address to identify the user.
try this, it may helpfull for you
<?php
//for example
$ip = "216.58.196.68:8989";
$ip = strstr("$ip",':',true); //get text before :
echo $ip;
//it echo only 216.58.196.68
?>

Change IP address (proxy) with test

I want to browse some sites using a proxy, so I want to change the IP address of my client to the specified IP.
I am also testing it on localhost, so my goal is to visit my localhost site with PHP script (also localhost). The problem is how to test this (maybe some simple test by writing out changed IP to file).
I'm not entirely sure what exactly you mean, but you can check your clients IP address with this:
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}
echo $ip;
Ofcourse this will not change your clients IP address. You need an actual proxy server for that.

How to get real IP of user using PHP? [duplicate]

This question already has answers here:
How to get the client IP address in PHP
(32 answers)
Closed 8 years ago.
I've been trying to get real IP address of the user, and not a proxy address. For that I've done this:
$ip1 = $_SERVER['REMOTE_ADDR'];
$ip2 = $_SERVER['HTTP_X_FORWARDED_FOR'];
$ip3 = $_SERVER['HTTP_FORWARDED'];
mail("me#domain.com", "Report", "IP1 is $ip1, IP2 is $ip2, IP3 is $ip3 .");
But when a user is using proxy, the above script gets the proxy address and not the real IP address:
IP is [proxy_addr_here], IP2 is , IP3 is .
Is there any way to get real IP just like whatismyip.com tells (it tells real IP address, proxy address and useragent)?
Update : Whatismyip tells me this
"Your IP Address Is: [my real IP]
Proxy: [my proxy address]
City: Alipur
State/Region: Delhi
Country: IN -
ISP: Bharti Airtel Ltd."
How come it gathers all the details so accurately but not my PHP script?
You could do
$ip= isset($_SERVER['HTTP_X_FORWARDED_FOR']) ?
$_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'];
But even that is not 100%. Getting IP of the real user now a days is not a guarantee with so many NAT firewalls in between, let alone proxies.
Edit
To answer your edit as to how they show you more information, you could use many features of the language to get more request details. For example try
print_r(getallheaders());
Then you can also use
print_r($_SERVER);
And extract the required information from there.
Edit 2
Even your favorite whatismyip.com reports
Your IP Location can be found using our IP Lookup tool. No IP Lookup tool is 100% accurate due to many different factors. Some of those factors include where the owner of the IP has it registered, where the agency that controls the IP is located, proxies, cellular IPs, etc. If you are in the US and the controlling agency of the IP is located in Canada, chances are the IP address lookup results will show as Canada. Showing a Canadian IP while in the US is very common among Blackberry users on the Verizon network.
Reference
You might still ask ok then why cant I get at least what they show? Im sure they have put up a lot of research and resources in setting up that tool, its not a one line PHP code, in fact there is no telling whether that site is written in PHP at all. Some more research and you will be on your way to try to match their lookup.
You can try this...
<?PHP
function getUserIP()
{
$ipaddress = '';
if (getenv('HTTP_CLIENT_IP'))
$ipaddress = getenv('HTTP_CLIENT_IP');
else if(getenv('HTTP_X_FORWARDED_FOR'))
$ipaddress = getenv('HTTP_X_FORWARDED_FOR');
else if(getenv('HTTP_X_FORWARDED'))
$ipaddress = getenv('HTTP_X_FORWARDED');
else if(getenv('HTTP_FORWARDED_FOR'))
$ipaddress = getenv('HTTP_FORWARDED_FOR');
else if(getenv('HTTP_FORWARDED'))
$ipaddress = getenv('HTTP_FORWARDED');
else if(getenv('REMOTE_ADDR'))
$ipaddress = getenv('REMOTE_ADDR');
else
$ipaddress = 'Unknown IP Address';
return $ipaddress;
}
$user_ip = getUserIP();
echo $user_ip; // Output IP address (Ex: 123.345.456.678)
?>

CloudFlare and logging visitor IP addresses via in PHP

I'm trying to track and log users/visitors that are accessing my website using PHP's $_SERVER['REMOTE_ADDR'] to do so. A typical method for IP address tracking in PHP.
However, I am using CloudFlare for caching and such and receiving their IP addresses as CloudFlare's:
108.162.212.* - 108.162.239.*
What would be a correct method of retrieving the actual users/visitors IP address while still using CloudFlare?
Extra server variables that are available to cloud flare are:
$_SERVER["HTTP_CF_CONNECTING_IP"] real visitor ip address, this is what you want
$_SERVER["HTTP_CF_IPCOUNTRY"] country of visitor
$_SERVER["HTTP_CF_RAY"]
$_SERVER["HTTP_CF_VISITOR"] this can help you know if its http or https
you can use it like this:
if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER["HTTP_CF_CONNECTING_IP"];
}
If you do this, and the validity of the visiting IP address is important, you might need to verify that the $_SERVER["REMOTE_ADDR"] contains an actual valid cloudflare IP address, because anyone can fake the header if he was able to connect directly to the server IP.
Update: CloudFlare has released a module mod_cloudflare for apache, the module will log and display the actual visitor IP Addresses rather than those accessed by cloudflare! https://www.cloudflare.com/resources-downloads#mod_cloudflare (Answer by: olimortimer)
If you dont have access to the apache runtime you can use the script below, this will allow you to check if the connection was through cloudflare and get the users ip.
I am rewriting my answer i used for another question "CloudFlare DNS / direct IP identifier"
Cloudflare's ips are stored in public so you can go view them here then check if the ip is from cloudflare (this will allow us to get the real ip from the http header HTTP_CF_CONNECTING_IP).
If you are using this to disable all non cf connections or vice versa, i recommend you to have a single php script file that gets called before every other script such as a common.php or pagestart.php etc.
function ip_in_range($ip, $range) {
if (strpos($range, '/') == false)
$range .= '/32';
// $range is in IP/CIDR format eg 127.0.0.1/24
list($range, $netmask) = explode('/', $range, 2);
$range_decimal = ip2long($range);
$ip_decimal = ip2long($ip);
$wildcard_decimal = pow(2, (32 - $netmask)) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return (($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal));
}
function _cloudflare_CheckIP($ip) {
$cf_ips = array(
'199.27.128.0/21',
'173.245.48.0/20',
'103.21.244.0/22',
'103.22.200.0/22',
'103.31.4.0/22',
'141.101.64.0/18',
'108.162.192.0/18',
'190.93.240.0/20',
'188.114.96.0/20',
'197.234.240.0/22',
'198.41.128.0/17',
'162.158.0.0/15',
'104.16.0.0/12',
);
$is_cf_ip = false;
foreach ($cf_ips as $cf_ip) {
if (ip_in_range($ip, $cf_ip)) {
$is_cf_ip = true;
break;
}
} return $is_cf_ip;
}
function _cloudflare_Requests_Check() {
$flag = true;
if(!isset($_SERVER['HTTP_CF_CONNECTING_IP'])) $flag = false;
if(!isset($_SERVER['HTTP_CF_IPCOUNTRY'])) $flag = false;
if(!isset($_SERVER['HTTP_CF_RAY'])) $flag = false;
if(!isset($_SERVER['HTTP_CF_VISITOR'])) $flag = false;
return $flag;
}
function isCloudflare() {
$ipCheck = _cloudflare_CheckIP($_SERVER['REMOTE_ADDR']);
$requestCheck = _cloudflare_Requests_Check();
return ($ipCheck && $requestCheck);
}
// Use when handling ip's
function getRequestIP() {
$check = isCloudflare();
if($check) {
return $_SERVER['HTTP_CF_CONNECTING_IP'];
} else {
return $_SERVER['REMOTE_ADDR'];
}
}
To use the script it's quite simple:
$ip = getRequestIP();
$cf = isCloudflare();
if($cf) echo "Connection is through cloudflare: <br>";
else echo "Connection is not through cloudflare: <br>";
echo "Your actual ip address is: ". $ip;
echo "The server remote address header is: ". $_SERVER['REMOTE_ADDR'];
This script will show you the real ip address and if the request is CF or not!
Cloudflare sends some additional request headers to your server including CF-Connecting-IP which we can store into $user_ip, if defined, using this simple one-liner:
$user_ip = $_SERVER["HTTP_CF_CONNECTING_IP"] ?? $_SERVER['REMOTE_ADDR'];
Since this question was asked and answered, CloudFlare has released mod_cloudflare for Apache, which logs & displays the actual visitor IP address rather than the CloudFlare address:
https://www.cloudflare.com/resources-downloads#mod_cloudflare
It would be hard to convert HTTP_CF_CONNECTING_IP to REMOTE_ADDR. So you can use apache (.htaccess) auto prepending to do that. So that you do not need to think about whether the $_SERVER['REMOTE_ADDR'] has the correct value in all the PHP scripts.
.htaccess code
php_value auto_prepend_file "/path/to/file.php"
php code (file.php)
<?php
define('CLIENT_IP', isset($_SERVER['HTTP_CF_CONNECTING_IP']) ? $_SERVER['HTTP_CF_CONNECTING_IP'] : $_SERVER['REMOTE_ADDR']);
Learn More here
HTTP_CF_CONNECTING_IP is only working if you are using cloudflare maybe you transfer your site or remove cloudflare you will forget the value so use this code .
$ip=$_SERVER["HTTP_CF_CONNECTING_IP"];
if (!isset($ip)) {
$ip = $_SERVER['REMOTE_ADDR'];
}
When you are using CloudFlare all your requests between your server and users are routed through CloudFlare servers.
In this case there are two methods to get User's Real IP Address:
Through Extra Server Headers added by CloudFlare Servers
Adding a CloudFlare Apache/NGINX Module on your server.
Method 1: Get IP though extra Server Headers
You can use the following code to get user's IP Address:
$user_ip = (isset($_SERVER["HTTP_CF_CONNECTING_IP"]) $_SERVER["HTTP_CF_CONNECTING_IP"]:$_SERVER['REMOTE_ADDR']);
How it is working?
CloudFlare adds some extra server variables in the request as follows:
$_SERVER["HTTP_CF_CONNECTING_IP"] - Real IP Address of user
$_SERVER["HTTP_CF_IPCOUNTRY"] - ISO2 Country of the User
$_SERVER["HTTP_CF_RAY"] A Special string for loggin purpose
In the above code, we are checking if $_SERVER["HTTP_CF_CONNECTING_IP"] is set or not. If it is there we will consider that as user's IP Address else we will use the default code as $_SERVER['REMOTE_ADDR']
Method 2: Installing Cloudflare Module on your server
Installing CloudFlare Module on Apache Server
Installing CloudFlare Module on NGINX Server
Installing CloudFlare Module on EasyApache+CPanel
In Laravel add the following line to AppServiceProvider:
...
use Symfony\Component\HttpFoundation\Request;
...
public function boot()
{
Request::setTrustedProxies(['REMOTE_ADDR'], Request::HEADER_X_FORWARDED_FOR);
}
Now you can get real client IP using request()->ip().
Read more here.
Cloudflare has an option to use a "Pseudo IPv4" address in the headers on the Network Management page for the domain. You have the option of adding or overwriting the headers that are sent to your server.
From the documentation:
What is Pseudo IPv4?
As a stopgap to accelerate the adoption of IPv6, Cloudflare offers Pseudo IPv4 which supports IPv6 addresses in legacy applications expecting IPv4 addresses. The goal is to provide a nearly unique IPv4 address for each IPv6 address, using Class E IPv4 address space, which is designated as experimental and would not normally see traffic. To learn more see here.
Options
Add header: Add additional Cf-Pseudo-IPv4 header only
Overwrite headers: Overwrite the existing Cf-Connecting-IP and X-Forwarded-For headers with a pseudo IPv4 address.
Note: We recommend leaving this set to “Off” unless you have a specific need.
You can learn more about this feature from this article from Cloudflare, where it goes into a little more detail about how they try to accommodate the 128-bit address space of IPv6 in the 32-bit space found in IPv4.
In the event that you would like to know the IPv6 address along with the pseudo IPv4, Cloudflare adds a Cf-Connecting-IPv6 header when Pseudo IPv4 is enabled. This can be used to measure how much of your traffic is originating from devices that are on IPv6 networks, which can be useful if you need a solid number to show management before investments are made in updating systems that are still bound to the IPv4 limitations.
It's better to check cloudflare ip ranges online (because it may change anytime) then check it's from cloudflare or not.
Cloudflare IP txt
If the source is Cloudflare you can use $_SERVER['HTTP_CF_CONNECTING_IP'] to get your client request ip-address but it's not safe to use it for all requests because it can send by any user in request header to trick you.
You can use following code to get real ip-address of your client request:
function _getUserRealIP() {
$ipaddress = '';
if(isset($_SERVER['REMOTE_ADDR']))
$ipaddress = $_SERVER['REMOTE_ADDR'];
else if (isset($_SERVER['HTTP_CLIENT_IP']))
$ipaddress = $_SERVER['HTTP_CLIENT_IP'];
else if(isset($_SERVER['HTTP_X_FORWARDED_FOR']))
$ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
else if(isset($_SERVER['HTTP_X_FORWARDED']))
$ipaddress = $_SERVER['HTTP_X_FORWARDED'];
else if(isset($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']))
$ipaddress = $_SERVER['HTTP_X_CLUSTER_CLIENT_IP'];
else if(isset($_SERVER['HTTP_FORWARDED_FOR']))
$ipaddress = $_SERVER['HTTP_FORWARDED_FOR'];
else if(isset($_SERVER['HTTP_FORWARDED']))
$ipaddress = $_SERVER['HTTP_FORWARDED'];
else
$ipaddress = 'UNKNOWN';
return $ipaddress;
}
function _readCloudflareIps()
{
$file = file("https://www.cloudflare.com/ips-v4",FILE_IGNORE_NEW_LINES);
return $file;
}
function _checkIpInRange($ip, $range) {
if (strpos($range, '/') == false)
$range .= '/32';
// $range is in IP/CIDR format eg 127.0.0.1/24
list($range, $netmask) = explode('/', $range, 2);
$range_decimal = ip2long($range);
$ip_decimal = ip2long($ip);
$wildcard_decimal = pow(2, (32 - $netmask)) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return (($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal));
}
function _checkIsCloudflare($ip) {
$cf_ips = _readCloudflareIps();
$is_cf_ip = false;
foreach ($cf_ips as $cf_ip) {
if (_checkIpInRange($ip, $cf_ip)) {
$is_cf_ip = true;
break;
}
}
return $is_cf_ip;
}
function getRealIp()
{
$httpIp = _getUserRealIP();
$check = _checkIsCloudflare($httpIp);
if ($check) {
return $_SERVER['HTTP_CF_CONNECTING_IP'];
}else{
return $httpIp;
}
}
After importing this functions to your code you just need to call getRealIp() function like:
$userIp = getRealIp();
echo $userIp();
For magento 1.x users (I haven't try magento 2.0 yet), check https://tall-paul.co.uk/2012/03/13/magento-show-remote-ip-in-cloudflare-the-right-way/ which needs to change app/etc/local.xml and add:
HTTP_CF_CONNECTING_IP
another way to get it in Laravel is simply by reading HTTP_CF_CONNECTING_IP header
$request->server('HTTP_CF_CONNECTING_IP')
you can read more about it here:
How to get real client IP behind Cloudflare in Laravel / PHP

Categories