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)
Jack,
ReplyDeleteThis looks it will be an interesting series. Good first item to tackle - ESAPI PHP. One thing I'd be interested in finding out is that status of the ESAPI PHP project. I originally had thought that it was sort of on hold / had stalled. But based on this post it sounds like it is alive and well.
Also, you may want to offer a download link for all of your source code so people can easily grab it all at once to follow along at home.
Nice first post.
Good choice for a first post. These pragmatic, quick-win type case studies can really help drive adoption of all the ESAPI projects. I'm looking forward to the rest of the series.
ReplyDeleteAlso, while you're putting this together make sure that you're planning a conference talk in the back of your head. A few more thoughtful posts and you'll find that you know more about this little corner of infosec than 95% of folks out there. ;)
~PST
@coffeetocode
Michael,
ReplyDeleteit's very much alive. I'm out of the picture at the moment with fairly terrible family problems, but I will return once that is settled. Mike B is doing a fantastic job.
Andrew
Hi,
ReplyDeleteThanks for the post. Just starting CodeIgniter and dealing with various security issues such as character encoding, null bytes, UTF-8, buffer overflow of vulnerable PHP extensions, multibyte strings, and so on. CodeIgniter already has an active record class so queries are already escaped, but encoding is proving to be a big headache.
Hopefully you continue this series, so people like me can figure out how to harden their PHP applications without reinventing the wheel ;). I'm hoping ESAPI will be the panacea to my woes.
Excellent first post Jack, well done!
ReplyDeleteLooking forward to the rest of this series and your future blog posts!
David Rook
Security Ninja
@securityninja
Michael,
ReplyDeleteThanks for the input. I updated the post with a link to the source code.
ESAPI PHP still seemed to be pretty active, even though the mailing list traffic has died down a bit. I saw commits as recently as about a month ago, so it seems there are still people working on it. It's a very cool project.
Hey Patrick,
ReplyDeleteThanks for the kind words! I always gravitate towards examples like this when I'm learning something new, so I hope this series will help some people decide to use ESAPI.
Hey Brian,
ReplyDeleteI have definitely reinvented the wheel a few times when it comes to this sort of thing, and not nearly as well :)
It's cool to find a project like this that makes all this a lot simpler.
Hey David,
ReplyDeleteThanks! Everyone has David to thank or blame for this blog because of the @infosecmentors program.
Great first post! Very easy to follow :) Looking forward to the series.
ReplyDeleteSeems to be it is going to be great a series even for newbies like me.
ReplyDeleteI had problem with comments. They just don't show up on main page and in DB coment's content_id is always 0. What can cause this?
Thanks! I'm actually not sure off hand why content id would be going in as zero, but I will take a look tonight at the week one code and see if there is a mistake. If so, I'll repost the file and make a note here.
ReplyDeleteGreat topic, Jack. And a wonderful first post, looking forward to the rest of this series.
ReplyDeletetnx for sharing!
Dave
Dave,
ReplyDeleteThank you for the kind words! The rest of the series is up now.
Thanks,
Jack