Wednesday, June 23, 2010

Using the OWASP PHP ESAPI - Part 1

OK, my first blog post! To kick this off, I'm going to do a series of posts about the Open Web Application Security Project (OWASP) PHP ESAPI. The OWASP PHP ESAPI is a free, open source, web application security control library that makes it easier for programmers to write lower-risk applications (taken straight from OWASP's site). It's fairly easy to use, and a great educational resource if you're willing to explore the source code a bit. Ideally, even if you're somewhat new to PHP, you should be able to follow this and learn a thing or two about securing a PHP web application.

Over the next three weeks, we're going to talk a bit about application security and what the PHP ESAPI is and is not.
Week 1: We will create a simple PHP blog application rife with security vulnerabilities. This is the sort of application you will find in many tutorials and beginner PHP books.
Week 2: We'll discuss some of these vulnerabilities and retrofit our application with the OWASP PHP ESAPI.
Week 3: The ESAPI is great and all, but there are some things that it just can't protect against, like logic errors. This week, we'll discuss the logic problems in our blog application and the limits of the ESAPI.

Just a note before we get started: I'll be demonstrating some very insecure code to start this series out. It would likely be a very big mistake to use this in any kind of of live environment. So with that said, let's get going.

We're going to start by building a simple, insecure PHP blogging application. We're not really interested in building something awesome, just something with enough of an attack surface to demonstrate some good use of the ESAPI. Our application will allow an admin user to post content to a page and update that content. Anyone else is allowed to comment on that content. Revolutionary, huh?

We can see from our description, we'll need a few different components:
1. A piece that will display the content and comments
2. A piece that will allow an authenticated user to enter content
3. A login piece that will allow a user to log in
4. A piece that will allow a visitor to enter a comment

We'll start by creating a simple database to house our application data. I'm using MySQL.
 USE insecure;  
 CREATE TABLE user (id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(16), password VARCHAR(16));  
 CREATE TABLE content (id INT AUTO_INCREMENT PRIMARY KEY, user_id INT, title VARCHAR(140), content TEXT, date_created TIMESTAMP);  
 CREATE TABLE comments (id INT AUTO_INCREMENT PRIMARY KEY, comment VARCHAR(140), content_id INT, date_created TIMESTAMP);  

Next, we'll create a few classes our application will use. A DB singleton class, a User class, a Comment class and a Content class. First up, our DB class. I made this whole thing available at insecure_app.tar.gz to make it easier to follow along at home.


<?php
  /**
   *
   * DB.php
   * 
   * This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
   * Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
   * this in any kind of production environment. 
   * 
   * @author jackwillk
   * @created 2010 
   * 
   */

class DB {
  
  /**
   * This class provides the barest wrapper for MySQL
   */ 

  private $host = "localhost";
  private $username = "root";
  private $password = "supersecretpw";
  private $db_name = "insecure";
  private static $instance;

  private function __construct() {
    $this->connect();
  }

  function connect() {
    $this->connection = mysql_connect($this->host, $this->username, $this->password);
    if(!$this->connection) {
      echo(mysql_error());
    }
    mysql_select_db($this->db_name);
  }

  function query($sql) {
    $result = mysql_query($sql, $this->connection);
    if(!$result) {
      echo(mysql_error());
    }
    return $result;
  }

  function count_rows($result) {
    return mysql_num_rows($result);
  }
  
  function fetch_assoc($result) {
    return mysql_fetch_assoc($result);
  }

  static function get_instance() {
    if(!self::$instance) {
      self::$instance = new DB;
    }
    return self::$instance;
    }

  }

?>


As it mentions in the comments, this is just the barest wrapper for the built in MySQL functions. We'll be fixing this up later. Next, our User class.

<?php
  /**
   *
   * User.php
   * 
   * This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
   * Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
   * this in any kind of production environment 
   * 
   * @author jackwillk
   * @created 2010
   *
   */

class User {
      private $username = null;
      private $password = null;
      private $user_id = null;

      function __construct($user_id='') {
 if($this->check_user_session()) {
   $this->set_username($_SESSION['username']);
   $this->set_user_id($_SESSION['user_id']);
 }
      }

      function get_user_id() {
 return $this->user_id;
      }

      function set_user_id($user_id) {
 $this->user_id = $user_id;
      }

      function get_username() {
 return $this->username;
      }

      function set_username($username) {
 $this->username = $username;
      }

      /**
       * This function will check a user submitted username and password against 
       * the database. If it exists, it sets up a new session
       */

      function login($username, $password) {
 $db = DB::get_instance();
 $sql = "select * from user where username = '$username' and password = '$password'";
 $result= $db->query($sql);
 if($db->count_rows($result)) {
   $row = $db->fetch_assoc($result);
   $this->set_user_id($row['id']);
   $this->set_username($row['username']);
   $this->create_user_session();
 }
      }

      /**
       * This function is used to start a session and set initial variables
       */ 

      private function create_user_session() {
 session_start();
 $_SESSION['user_id'] = $this->get_user_id();
 $_SESSION['username'] = $this->get_username();
      }

      /**
       * This function checks to see if a user is logged in. 
       */

      function check_user_session() {
 session_start();
 if($_SESSION['user_id'] && $_SESSION['username']) {
   return true;
 }
 return false;
      }
}

?>

This class basically handles the login and session stuff. I like to explicitly define the accessor methods instead of using PHP's built in magic functions for this. I feel that it makes the code cleaner and easier to read and we can also add validation into them later. But for now, we'll stick with making this app as vulnerable and possible, so no validation!

Now we'll build or Content class to handle the blog posts.

<?php
  /**
   *
   * Content.php
   * 
   * This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
   * Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
   * this in any kind of production environment 
   * 
   * @author jackwillk
   * @created 2010
   *
   */


class Content {
  private $content_id = null;
  private $user_id = null;
  private $title = null;
  private $content = null;
  private $date_created = null;

  function __construct($content_id = '', $user_id = '', $title = '', $content = '', $date_created = '') {
    if($content_id) {
      $this->content_id = $content_id;
      $this->retrieve_content();
    } else {
      $this->user_id = $user_id;
      $this->title = $title;
      $this->content = $content;
      $this->date_created = $date_created;
    }
  }

  function get_content_id() {
    return $this->content_id;
  }

  function set_content_id($content_id) {
    $this->content_id = $content_id;
  }

  function get_user_id() {
    return $this->user_id;
  }

  function set_user_id($user_id) {
    $this->user_id = $user_id;
  }

  function get_title() {
    return $this->title;
  }

  function set_title($title) {
    $this->title = $title;
  }

  function get_content() {
    return $this->content;
  }
  
  function set_content($content) {
    $this->content = $content;
  }

  function get_date_created() {
    return $this->date_created();
  }

  function set_date_created($date_created) {
    $this->date_created = $date_created;
  }

  private function retrieve_content() {
    $db = DB::get_instance();
    $sql = "SELECT * FROM content WHERE id = " . $this->content_id;
    $result = $db->query($sql);
    $row = $db->fetch_assoc($result);
    $this->__construct('', $row['user_id'], $row['title'], $row['content'], $row['date_created']);
  }

  function write() {
    $db = DB::get_instance();
    $sql = "INSERT INTO content (user_id, title, content, date_created) values ('" . $this->user_id . "', '" . $this->title . "', '" . $this->content . "', '" . date("Y-m-d") . "')";
    $result = $db->query($sql);
  }

  }

?>

Standard class with create and read functions. Our Comment class is similar:

<?php
  /**
   *
   * Comment.php
   * 
   * This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
   * Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
   * this in any kind of production environment 
   * 
   * @author jackwillk
   * @created 2010
   *
   */

class Comment {

  private $comment_id = null;
  private $comment = null;
  private $content_id = null;
  private $date_created = null;

  function __construct($comment_id='', $comment='', $content_id='', $date_created='') {
    $this->set_comment_id($comment_id);
    $this->set_comment($comment);
    $this->set_content_id($content_id);
    $this->set_date_created($date_created);
  }

  function get_comment_id() {
    return $this->comment_id;
  }

  function set_comment_id($comment_id) {
    $this->comment_id = $comment_id;
  }

  function get_comment() {
    return $this->comment;
  }

  function set_comment($comment) {
    $this->comment = $comment;
  }

  function get_content_id() {
    return $this->content_id;
  }

  function set_content_id($content_id) {
    $this->content_id = $content_id;
  }

  function get_date_created() {
    return $this->date_created;
  }
  
  function set_date_created($date_created) {
    $this->date_created = $date_created;
  }

  function write() {
    $db = DB::get_instance();
    $sql = "insert into comments (comment, content_id, date_created) values ('" . $this->comment . "', '" . $this->content_id . "', '" . date("Y-m-d") . "')";
    $result = $db->query($sql);
  }

}

?>

And that does it for our classes, easy huh? Now we just need a controller for each of our pages and our HTML. The first page we'll deal with is our main blog page.

<?php

  /**
   *
   * index.php
   * 
   * This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
   * Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
   * this in any kind of production environment 
   * 
   * @author jackwillk
   * @created 2010
   *
   */
require("lib/DB.php");
require("lib/User.php");
require("lib/Content.php");
require("lib/Comment.php");

$db = DB::get_instance();

function get_all_content() {
  $db = DB::get_instance();
  $sql = "SELECT * FROM content order by date_created";
  $result = $db->query($sql);
  while($row=$db->fetch_assoc($result)) {
    $content_arr[] = new Content($row['id'], $row['user_id'], $row['title'], $row['content'], $row['date_created']);
  }
  return $content_arr;
}


function get_all_comments($content_id) {
  $db = DB::get_instance();
  $sql = "SELECT * FROM comments where content_id = '$content_id' order by date_created";
  $result = $db->query($sql);
  while($row = $db->fetch_assoc($result)) {
    $comment_arr[] = new Comment($row['id'], $row['comment'], $row['content_id'], $row['date_created']);
  }
  return $comment_arr;
}

function construct_content_display($content_arr) {
  $output = '';
  for($i=0;$i<count($content_arr);$i++) {
    $output .= "<p><b>" . $content_arr[$i]->get_title() . "</b></p>";
    $output .= "<br><p>" . $content_arr[$i]->get_content() . "</p>";
    $comment_arr = get_all_comments($content_arr[$i]->get_content_id());
    for($j=0;$j<count($comment_arr);$j++) {
      $output .= "<br><p>" . $comment_arr[$j]->get_comment() . " - " . $comment_arr[$j]->get_date_created() . "</p>";
    }
    $output .= "<br><a href=\"comment.php?content_id=" . $content_arr[$i]->get_content_id() . "\">Comment</a>";
  }
  return $output;
}

$user = new User();
$content_arr = get_all_content();

include("index.html");

?>


And our HTML file looks like:

<html>
  <head>
    <title>Insecure!</title>
  </head>
  <body>
    <a href="login.php">Login</a><br>
    <?php if($user->get_user_id()) { ?>
    <a href="post.php">Post New Content</a><br><br>
    <?php } ?>
    <?php  echo(construct_content_display($content_arr)); ?>
  </body>
</html>

And our login page:

<?php

  /**
   *
   * login.php
   * 
   * This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
   * Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
   * this in any kind of production environment 
   * 
   * @author jackwillk
   * @created 2010
   *
   */

require("lib/DB.php");
require("lib/User.php");

$db = DB::get_instance();

$user = new User();
if($user->get_user_id()) {
  echo("You are already logged in.");
  exit();
 }

if($_POST['submit']) {
  $user->login($_POST['username'], $_POST['password']);
  if($user->get_user_id()) {
    header("Location:index.php");
  } else {
    echo("Username or password was incorrect<br>");
  }
 }

include("login.html");

?>

And its html

<html>
<head>
<title>Login</title>
</head>
<body>
<b>This is my awesome login page!</b>
<form name="login" method="post" action="login.php">
  username:<input type="text" name="username"><br>
  password:<input type="text" name="password"><br>
  <input type="submit" name="submit" value="submit">
</form>
</body>
</html>

The page where we'll post to our blog

<?php
  /**
   *
   * post.php
   * 
   * This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
   * Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
   * this in any kind of production environment 
   * 
   * @author jackwillk
   * @created 2010
   *
   */
require("lib/DB.php");
require("lib/User.php");
require("lib/Content.php");
require("lib/Comment.php");

$db = DB::get_instance();
if($_POST['submit']) {
  $content = new Content();
  $content->set_user_id($_POST['user_id']);
  $content->set_title($_POST['title']);
  $content->set_content($_POST['content']);
  $content->write();
  header("Location:index.php");
 }
$user = new User();
include("post.html");
?>

And its HTML

<html>
  <head>
    <title>Post</title>
  </head>
  <body>
    Post new content<br>
    <form name="content" method="post" action="post.php">
      Title: <input type="text" name="title"><br>
      Content: <textarea name="content"></textarea><br>
      <input type="hidden" name="user_id" value="<?= $user->get_user_id() ?>">
      <input type="submit" name="submit" value="submit">
    </form>
  </body>
</html>

And finally, our comment page

<?php
  /**
   *
   * comment.php
   * 
   * This code is part of a tutorial on using the Open Web Application Security Project (OWASP)
   * Enterprise Security API (ESAPI) project. It is extremely insecure! Please do not use
   * this in any kind of production environment 
   * 
   * @author jackwillk
   * @created 2010
   *
   */
require("lib/DB.php");
require("lib/User.php");
require("lib/Content.php");
require("lib/Comment.php");

$db = DB::get_instance();


if($_POST['submit']) {
  //save the comment
  $comment = new Comment();
  $comment->set_comment($_POST['comment']);
  $comment->set_content_id($_POST['content_id']);
  $comment->write();
  header("Location:index.php");
 }

$content_id = $_GET['content_id'];

if(!$content_id) {
  echo("Missing content id");
  exit();
 }
include("comment.html");

And its HTML

<html>
  <head>
    <title>Comment</title>
  </head>
  <body>
    Comment on <?= $content->get_title ?>
    <form name="comment" method="post" action="comment.php">
      Comment:<textarea name="comment"></textarea>
      <input type="hidden" name="content_id" value="<?= $content_id ?>">
      <input type="submit" name="submit" value="submit">
    </form>
  </body>
</html>

And there you have it! One extremely insecure blog application. I think that's enough for my first blog post! Next week, we'll begin discussing what's wrong with this mess and how to secure it using the OWASP PHP ESAPI.


I am pretty new at this whole blogging thing, and I would love to hear some feedback. So feel free to leave a comment, send me an email (jackwillksecurity at gmail dot com) or reach out to me on Twitter (@jackwillk)