Category Archives: PHP

Secure and extend you Philips Hue bridge with a reverse proxy

Philips Hue bridge

Recently I bought a Hue bridge with two bulbs, it was a specific “cheap” pack around 50€, the bridge itself usually costs that price, so I was quite interested. My idea was just to play with those bulbs and do funny things such as blinking when I get new mail, etc … After messing with it for a while using the Philips HUE app on Android, I wanted to do more so I checked the API. It’s quite well made and allows you to do a lot on your own, though, I had in mind to hide my bridge in my own local network, for security purpose and also to add more functionalities to it, let’s see how to make a hue bridge reverse proxy !

Requirements

To do that, you need:

  • a router, your own home box is enough
  • a raspberry PI or any computer with Apache and PHP
  • (optional) a USB to ethernet adapter, I bought this cheap one
  • some time to configure it all !

Configure the bridge

Follow the official instructions to install your bridge, you have to connect it to your router, it needs an IP within your local network so we can reach it with an other computer.

Once it’s all set and that it got an IP, open up your web browser and go to the following URL: http://192.168.1.xxx/debug/clip.html, obviously set the IP address to the correct one.

Create a new user following the steps on the API documentation. Keep the generated hash around, it’s important to control your bridge.

Stay in your web browser in the debug and do a GET call to http://<ip-address.of.the.bridge>/api/<username>/config, it will look like that:

Get the bridge information

Get the bridge information

Copy the mac field and save it somewhere, it’s important.

Finally, we will configure the bridge to stop DHCP and also to take the IP we want it to get out of the local range.

Still in your browser, do a PUT request to http://<ip-address.of.the.bridge>/api/<username>/config with the following content:

{"ipaddress":"10.50.0.2", "dhcp":false, "netmask": "255.255.255.0", "gateway": "10.50.0.1" }
Do a PUT request to http://<ip-address.of.the.bridge>/api/<username>/config

Once you run it, you should lose the control to the bridge ! No worries, we’ll get it back.

Network adapter

If you bought the network adapter I suggested, it should look like that:

Network adapter in its blister

Network adapter in its blister

SSH to your raspberry pi, and check the network configuration:

 # ifconfig -a
 eth0      Link encap:Ethernet  HWaddr XX:XX:XX:XX:XX:XX
           inet adr:192.168.1.3  Bcast:192.168.1.255  Masque:255.255.255.0
           UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
           RX packets:1575384 errors:0 dropped:62 overruns:0 frame:0
           TX packets:810579 errors:0 dropped:0 overruns:0 carrier:0
           collisions:0 lg file transmission:1000
           RX bytes:271762165 (259.1 MiB)  TX bytes:189260556 (180.4 MiB)
 
 lo        Link encap:Boucle locale
           inet adr:127.0.0.1  Masque:255.0.0.0
           UP LOOPBACK RUNNING  MTU:65536  Metric:1
           RX packets:4781492 errors:0 dropped:0 overruns:0 frame:0
           TX packets:4781492 errors:0 dropped:0 overruns:0 carrier:0
           collisions:0 lg file transmission:1
           RX bytes:402951806 (384.2 MiB)  TX bytes:402951806 (384.2 MiB)
ifconfig -a before plugging the adapter

Now plug the network adapter USB side to your PI, and connect the ethernet cable to the ethernet adapter.

Network adapter plugged

Network adapter plugged

We’ll check the adapter is working by doing the same command, but now we should see eth1 !

 # ifconfig -a
 eth0      Link encap:Ethernet  HWaddr XX:XX:XX:XX:XX:XX
           inet adr:192.168.1.3  Bcast:192.168.1.255  Masque:255.255.255.0
           UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
           RX packets:1576244 errors:0 dropped:62 overruns:0 frame:0
           TX packets:811252 errors:0 dropped:0 overruns:0 carrier:0
           collisions:0 lg file transmission:1000
           RX bytes:271834756 (259.2 MiB)  TX bytes:189382050 (180.6 MiB)
 
 lo        Link encap:Boucle locale
           inet adr:127.0.0.1  Masque:255.0.0.0
           UP LOOPBACK RUNNING  MTU:65536  Metric:1
           RX packets:4781492 errors:0 dropped:0 overruns:0 frame:0
           TX packets:4781492 errors:0 dropped:0 overruns:0 carrier:0
           collisions:0 lg file transmission:1
           RX bytes:402951806 (384.2 MiB)  TX bytes:402951806 (384.2 MiB)

 eth1      Link encap:Ethernet  HWaddr XX:XX:XX:XX:XX:XX
           BROADCAST MULTICAST  MTU:1500  Metric:1
           RX packets:0 errors:0 dropped:0 overruns:0 frame:0
           TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
           collisions:0 lg file transmission:1000
           RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
ifconfig -a after plugging the adapter

Perfect, now let’s configure the network, edit the file /etc/network/interfaces by adding the following:

 auto eth1
 iface eth1 inet static
         address 10.50.0.1
         netmask 24
/etc/network/interfaces

Bring up eth1 end ping your bridge:

#  ifup eth1
 # ping 10.50.0.2
 PING 10.50.0.2 (10.50.0.2) 56(84) bytes of data.
 64 bytes from 10.50.0.2: icmp_seq=1 ttl=64 time=1.86 ms
 64 bytes from 10.50.0.2: icmp_seq=2 ttl=64 time=1.45 ms
 ^C
 --- 10.50.0.2 ping statistics ---
 2 packets transmitted, 2 received, 0% packet loss, time 1001ms
 rtt min/avg/max/mdev = 1.454/1.661/1.868/0.207 ms
Bring up eth1 and ping your bridge !

If it all works, you can move on to the HTTPS part, else, try using some networking tools such as tcpdump to determine what goes wrong.

HTTPS

New release of the Hue bridge uses HTTPS, of course it can’t rely on a real domain name since it’s connecting on the IP of the bridge and it can be different, after doing some reverse engineering it I found out how it works. Actually every bridge as an ID. Now that the link between your PI and the bridge is UP you can get your ID easily and prepare to create your own HTTPS certificate.

# openssl s_client -showcerts -connect 10.50.0.2:443 </dev/null
...
---
Server certificate
subject=/C=NL/O=Philips Hue/CN=00xxxxxxxx
issuer=/C=NL/O=Philips Hue/CN=00xxxxxxxx
---
...
Check what's the CN of your bridge

So there you go you have your CN which is actually what’s necessary to create your own HTTPS certificate. If you want to change your ID, you can, you’ll have to change it also in the PHP below.

First create a directory to store your key, certificate.

# mkdir -p /etc/ssl/hue
Create the /etc/ssl/hue directory

Now the key and the certificate

# openssl req -newkey rsa:4096 -nodes -keyout key_hue.pem -x509 -days 3650 -out certificate_hue.pem
Create your self signed certificate

The only parameter that matters here is the CN, put a correct bridge ID, in my case I just changed a few values.

Finally just concat both files to create a pem.

# cat certificate_hue.pem key_hue.pem > pem_hue.pem
Create the full certificate

A word of advice, as of now (version 1806051111 of the bridge), the hue app will use HTTPS to connect to the bridge, the first time you validate the connection it will stick the certificate. If you ever change the certificate, you’ll have to remove the credentials in your Android/iOS (i.e clear all data of the app) and press on the button again.

Second word of advice, if you test a lot, be careful to keep clean your whitelist user, it gets messy very fast ! You can delete some doing a DELETE request on /api/userYouControl/config/whitelist/userYouWishTodelete

Reverse proxy

Do you remember the MAC address of your bridge ? If, as I previously said you did, then we will use it pretty soon.

Stay on your PI and open up again /etc/network/interfaces, we will change the mac address of eth0 so if it will be seen as a HUE bridge for the mobile apps. You can use the mac address of the real bridge and alter it so it’s different, I suggest you use this website to make it sure it’s still seen as “Philips Lighting BV“.

auto eth0
iface eth0 inet static
        address 192.168.1.2
        netmask 24
        gateway 192.168.1.254
        dns-nameservers 127.0.0.1
        # faking MAC address to Philips HUE style
        hwaddress ether 00:17:88:78:45:12
Change the mac address

Now on your home box, set the IP of your PI as a static one for this tutorial it will be 192.168.1.3.

It’s time to install haproxy, apache and php, I won’t describe this here, do as you wish, we’ll just go through what’s really important.

There goes a valid haproxy configuration:

# Faking Hue Bridge requires HTTPS now

frontend f_http_hue
        mode http
        bind 192.168.1.3:80
        use_backend b_http_hue

frontend f_https_hue
        mode http
        bind 192.168.1.3:443 ssl crt /etc/ssl/hue/pem_hue.pem
        use_backend b_http_hue

backend b_http_hue
        mode http
        server bridge 127.0.0.1:8080
/etc/haproxy/haproxy.cfg

Install mod_proxy and mod_http_proxy, create a new virtualhost /etc/apache2/sites-available/hue.conf this way:

<VirtualHost 127.0.0.1:8080>
    DocumentRoot /var/www
    ServerName xxxxxxx
    CustomLog /var/log/apache2/hue_access.log combined env=!forwarded
    CustomLog /var/log/apache2/hue_access.log proxy env=forwarded
    ErrorLog /var/log/apache2/hue_error.log

    RewriteEngine On
    RewriteCond %{REQUEST_URI}  ^$ [OR]
    RewriteCond %{REQUEST_URI}  ^/$ [OR]
    RewriteCond %{REQUEST_URI}  \.png$ [OR]
    RewriteCond %{REQUEST_URI}  \.xml$ [OR]
    RewriteCond %{REQUEST_URI}  ^/debug/clip.html
    RewriteRule (.*)              http://10.50.0.2$1    [P,L]

    RewriteRule ^(/api.*)$ /index.php?q=$1 [L,QSA]
</VirtualHost>
/etc/apache2/sites-available/hue.conf

Activate it and edit /var/www/index.php:

<?php
/*
* Activate proxy_module and proxy_http_module
*/

ignore_user_abort(true);

$mac_address = [ '<mac address of the real bridge>',  '<your fake Philips HUE mac address>' ];
$ip_hue = [ '<ip of the real bridge>', '<ip of your PI>' ];
$bridge_id = [ '<the real bridge id>', '<faked id bridge>' ];
$gateway_hue = [ '<ip of your PI>', '<gateway of your local network>' ];
$url = 'http://10.50.0.2'.$_SERVER['REQUEST_URI'];
$useDB = false;

$options =[ 
    'http' => [
        'header'  => "Accept-Encoding: gzip, deflate\r\nAccept-language: en-US,en;q=0.8\r\nUser-Agent: ".$_SERVER["HTTP_USER_AGENT"]."\r\n",
        'method'  => $_SERVER["REQUEST_METHOD"],
    ]
];

if($_SERVER["REQUEST_METHOD"] !== 'GET' ){
    $options['http']['header'] .= "Content-type: application/x-www-form-urlencoded\r\n";
    $options['http']['content'] = file_get_contents("php://input");
}

$context  = stream_context_create($options);
$result = file_get_contents($url, false, $context);
if ($result === FALSE) {  }

// faking HUE bridge EDIT: 13/08, it needs to be faked all the time now
$result = str_replace($mac_address[0], $mac_address[1], $result);
$result = str_replace($ip_hue[0], $ip_hue[1], $result);
$result = str_replace($gateway_hue[0], $gateway_hue[1], $result);
$result = str_replace($bridge_id[0], $bridge_id[1], $result);

ob_start();
echo $result;
$size = ob_get_length();
header("Content-Length: {$size}");
header("Connection: close");
ob_end_flush();
ob_flush();
flush();

/** DB part **/
if($useDB){

    $_user = '';
    $_password = '';
    $_database = '';
    $_host = '';

    try {
	$_db_link = new PDO('mysql:host='.$_host.';dbname='.$_database.';charset=utf8', $_user, $_password);
    } catch (Exception $e) {
        die('Erreur : ' . $e->getMessage());
    }

    $stmt = $_db_link->prepare("INSERT INTO hue_log (ip, method, url, content) VALUES (:ip, :method, :url, :content)");
    $stmt->bindParam(':ip', hash('sha256', $_SERVER['REMOTE_ADDR']));
    $stmt->bindParam(':method', $_SERVER['REQUEST_METHOD']);
    $stmt->bindParam(':url', $_SERVER['REQUEST_URI']);
    $stmt->bindParam(':content', $result);
    $stmt->execute();
}

Now restart Apache and open your browser on http://192.168.1.3, it should load ! Check the logs of Apache, you should also see some requests.

Philips’ upgrades

I didn’t mention it, because I hadn’t figure it yet, but all the Philips’ Upgrades won’t be done anymore since the bridge has no internet access. I found out one way to do it, it’s tricky but anyone can do it.

I use the Hue App on my mobile to control lights, it tells you when some upgrades are required, it’s how I know I should run them. When it happens, I simply forward packets from my bridge to my PI and force the update thought the API, let’s do it.

Allow packet forwarding and forward the bridge to the PI:

 echo 1 > /proc/sys/net/ipv4/ip_forward
 iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
 iptables -A FORWARD -i eth0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT
 iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT
Allow kernel forwarding and forward bridge to the PI

You should notice that the third light is now lit, it reaches internet, no worries we open it up only temporarily.

Connect to the API and run the following commands:

 {
         "portalservices": true
 }
Do a PUT request to http://<ip-address.of.the.bridge>/api/<username>/config

{"swupdate": {"checkforupdate":true}}
Do a PUT request to http://<ip-address.of.the.bridge>/api/<username>/config

 {
         "swupdate": {
                     "updatestate": 3
                         }
 }
Do a PUT request to http://<ip-address.of.the.bridge>/api/<username>/config

It should download the update, restart your bridge, launch the update from your mobile app, sometimes it’s quite long ! To update three bulbs it took me around 1 hour, it depends on the update.

Once everything is done, cut the bridge from the internet:

 iptables -F nat
 iptables -F FORWARD
 echo 0 > /proc/sys/net/ipv4/ip_forward
Clean up the firewall and shut down forwarding

Reboot it once again so remaining connections will be stopped. Only two lights should remain.

You might check that your bridge is really off the internet by doing a GET request to http://<ip-address.of.the.bridge>/api/<username>/config, you should have that:

     "portalstate": {
         "signedon": false,
         "incoming": false,
         "outgoing": false,
         "communication": "disconnected"
     },
     "internetservices": {
         "internet": "disconnected",
         "remoteaccess": "disconnected",
         "time": "disconnected",
         "swupdate": "disconnected"
     },
Content of the GET to http://<ip-address.of.the.bridge>/api/<username>/config

Also, it’s possible you do a tcpdump to check to what your bridge tries to contact, it might amuse you.

Sources for the upgrades:

Epilogue

Your HUE bridge is hidden behind your raspberry PI, it’s secure moreover you can also edit the PHP to add new functionality !

How to restrict php directives using php_admin_value and php_admin_flag

Let’s say you are dealing with a shared Apache server running PHP as an Apache module, you have several websites running, and you don’t want to let some php directives being changed. You allow the modifications of the directives such as  display_errors but you don’t want memory_limit to be changed by any of the users (let’s say 128M) also, you don’t want to say that your server runs PHP (i.e expose_php off).

It’s possible to change the values of the php.ini directives in three differents ways:

  • apache configuration (i.e virtualhost(s), apache2.conf, etc …)
  • .htaccess (if AllowOverride Options or AllowOverride All)
  • ini_set function

There are two ways to force the values of the php directives and make them unchangeable from .htaccess or with ini_set function.

php_admin_value <setting> <value>

This one forces a directive to a certain value (non boolean), for example:

php_admin_value memory_limit 128M

We force memory_limit to 128M.

php_admin_flag <setting> <on|off>

This one does the same but for a boolean value, for example:

php_admin_flag expose_php off

There the directive expose_php will be unset.

To do this for ALL the websites on your server, you can edit your /etc/apache2/apache2.conf (may change if you have httpd) and put this content at the end:

Include php_restrictions

Create the file  /etc/apache2/php_restrictions, and add this content:

php_admin_value memory_limit 200M
php_admin_flag expose_php off

You can add as many rules as you wish, then reload your webserver like that (may change if you have httpd):

/etc/init.d/apache2 reload

To do this for only ONE specific website (i.e virtualhost), just set the rules inside the virtualhost like that:

NameVirtualHost www.test.com:80
<VirtualHost www.test.com:80>
    ServerName test.com
    ServerAlias www.test.com 
    DocumentRoot /var/www/test
    # now the rules
    php_admin_value memory_limit 128M
    php_admin_flag expose_php off
</VirtualHost>

This will apply only to the www.test.com website.

Then you need to reload the webserver too.

Voilà, you have restricted some directives and some others are still changeable.

Though, some directives cannot be changed inside apache2.conf or virtualhost(s), you may check the documentation .

Use tinymce in CakePHP with a behavior

Hey !
I went through the problem of using WYSIWYG to have more nicer news, comments … well giving a new possibility to the users to personalize what they want to write.

I tried two possibilities : making my own BBCODE or using a WYSIWYG editor. I will talk a “bit” about these two options, if you only want the explanations about how to set it for your CakePHP, skip it and go to this section Tinymce, HtmlPurifier and CakePHP behavior.

BBCODE
It is a very good way to be protected from xss or any other malicious content because you convert directly the bbcode into (x)html. Though, it’s a bit annoying to use/do and limits the possibilities. Also, one good point against is the fact that you have to “decode” (i.e to convert it in (x)html) each time you want to display it, if you do it once when you save the content, you can’t reverse the process, and you get (x)html when you want to edit your content. To be true, you could reverse it, but it’s not the wisest way.

WYSIWYG
Well, it is pretty much better here. We directly work with (x)html which is easier. We save directly (x)html in the database when we create our news, comment, … and we just display the content when we want, no process. BUT ! it’s plain (x)html we have and it means the users can write what they want, your layout can be broken, or worst they can write some javascript and mess your website.

Obviously I ended up by using a WYSIWYG editor, tinymce but as I said previously, you have some good advantages, the good looking, the ease of use but a vulnerability : xss attack. So the goal is to prevent it and secure the content the users will write. There we won’t use htmlentities because the goal is to “execute” the html by the browser, so we have to get some tags, and get rid of the others. We could use strip_tags, but let’s see it’s not the safer way.

Let’s admit we allow the tag <p>, here is what we could do.

    $data = "<p onmouseover=alert("xss")>blablabla</p>";
    $data = strip_tags($data, '<p>');
    echo $data;

Put your mouseover the <p> tag (i.e blablabla) and you will see that “xss” pop up. Well it’s logic because we allow the <p> tag, would be the same with <span> and so on. It means we can allow some tags but not be totally safe.

HtmlPurifier

So let’s use HtmlPurifier, it’s a filtering PHP library which “purifies” your html, it even deals with malformed tags and follows the specifications ! It’s THE massive weapon ! You can do a whitelist of the acceptable tags and even decide which attributes of these tags are allowed. Well I discovered it few days ago, so I’m no expert but we’ll get down to some configuration.

Tinymce, HtmlPurifier and CakePHP behavior

I’ll explain here how to set tinymce and htmlpurifier with CakePHP using a behavior. First we need a copy of tinymce, you can find it on their website : tinymce, take the latest production version. Then you can put it in your webroot/js or you create your own folder “lib” in the webroot, do as you wish it doesn’t matter as long as it is in the webroot. Next step is to configure the tinymce editor to fit our needs. Create a file “config.js” in your tinymce folder and put what follows into.

 
tinyMCE.init
(
    {
        // General options
        mode : "textareas",
        theme : "advanced",
        plugins : "paste",
        width : "100%",
        height : "100%",

       // Theme options
       theme_advanced_buttons1 : "bold,italic,underline,strikethrough,forecolor,|,justifyleft,justifycenter,justifyright,justifyfull,|,bullist,numlist,|,undo,redo",
       theme_advanced_toolbar_location : "top",
       theme_advanced_toolbar_align : "left",
       theme_advanced_statusbar_location : "bottom",
       theme_advanced_resizing : false,

      // Paste plugins configuration
      paste_auto_cleanup_on_paste : true,
      paste_remove_styles: true,
      paste_remove_styles_if_webkit: true,
      paste_strip_class_attributes: true,
      paste_retain_style_properties : "none",
      theme_advanced_disable : "styleselect",
      convert_fonts_to_spans: false,

      // No css
      content_css : false
    }
);

Here what’s important is the field “theme_advanced_buttons1” which is the list of the tags we display in our editor, so the ones we let our users use. It has to fit with the configuration of HtmlPurifier we will do just after. What we accept is basic things such as list, text position, underline, bold, etc …

P.S : I added the plugin “paste”, very useful when u copy/paste stuff in the text editor, there it will clean the content, or keep what’s allowed.

P.S2 : Tinymce has a lot of parameters, you can really set a lot of things. There I show a “little” example.

Now HtmlPurifier ! First, we need the latest copy that we can download here. I decided to wrap it in a CakePHP behavior.  Why? Because I think it’s “smarter”, when we add/edit a comment, post, … we purify the content before to save it in the database. If you read well, you saw before and save. And yes it’s a callback method of the CakePHP model. Consequently, we will create a behavior that we will link to all our models that need it and through the beforeSave callback, we will purify.

Thus, let’s put our HtmlPurifier folder in the vendors. Rename your folder “HtmlPurifier-blabla” to only “htmlpurifier”, it’s just more convenient.

We can create a behavior in app/Model/Behavior, under the name of PurifyBehavior.php, and it should look like this.

 
class PurifyBehavior extends ModelBehavior
{
    function setup(Model $model, $settings = array())
    {
        //field is the field we purify by default it is called content
        $field = (isset($settings['field']))?$settings['field']:'content';
        $this->settings[$model->alias] = array('field' => $field);
    }

    /*
        cleaning before saving
    */
    function beforeSave(Model $model)
    {
        //convenient to get the name of the field to clean
        $field = $this->settings[$model->alias]['field'];

        //check if we are working on the field
        if(isset($model->data[$model->alias][$field]))
        {
            //get htmlpurifier => http://htmlpurifier.org/
            App::import('Vendor', null, array
            (
                'file' => 'htmlpurifier'.DS.'library'.DS.'HTMLPurifier.auto.php'
             ));

            //set its configuration
            $config = HTMLPurifier_Config::createDefault();
            $config->set('Core.Encoding', 'UTF-8');
            $config->set('AutoFormat.RemoveEmpty', true);
            $config->set('Core.EscapeNonASCIICharacters', false);

            /*
            allowed HTML elements according to tinymce's configuration => p[align] to allow p and the attribute align
            */
            $allowedHTML = "em,strong,p[style],ul,ol,li,span[style]";
            $config->set('HTML.Allowed', $allowedHTML);

            //cleaning
            $purifier = new HTMLPurifier($config);
            $model->data[$model->alias][$field] = $purifier->purify($model->data[$model->alias][$field]);
        }

        return true;
    }
}

We use the setup method to tell the behavior which field we want to purify, let’s say you do a news, you call the content field “body” and if you do a media, you could call it “description”, it’s why I did it this way. We will see how to set it in the model below. By default, the field value would be “content”. Then we use the callback beforeSave to purify. First we put in a variable the name of the field, it’s “more readable”. We check that the field exists. Why? For one good reason. The callback is fired each time you save something. Let’s say you want to save one field only with saveField, the behavior will fail because the field of the PurifyBehavior will not be set. We import the htmlpurifier library from the vendors.  Then create a default configuration. We set few parameters, encoding, removeEmpty (let’s say you have a span looking this <span></span>, htmlpurifier will erase it), etc … check the documentation for more parameters. Important there is the allowedHTML where we write all the tags and theirs attributes according to tinymce’s previous configuration ! It’s pretty easy to understand and to change, right? Finally we purify our field of the current model. Ensure of returning true or the save will fail !

P.S : You can of course add some conditions to check and fail the save by returning false. You may use $model->invalidate(‘name of the field’, ‘message to display’) to invalidate the field in your form and display an error message.

Last but not least, we have to do a small modification of all our models using the behavior. To tell them they have to behave with the purifybehavior. Let’s see how to do this, it’s the easiest and smallest part.

    public $actsAs = array('Purify' => array('field' => 'description'));

Pretty easy ! We just tell our model to use the PurifyBehavior and we set the field to “description” here. If your field’s name is “content” you don’t even have to set the array ! It would give something like this.

    public $actsAs = array('Purify');

Et voilà, you have proper html saved in your database, your users are less limited and your website is safer.

P.S : I didn’t say totally safe !