Sonntag, 22. März 2015

C# Chat Client v2

In diesem Post möchte ich eine etwas verbesserte Version des Chat Clients aus dem vorigen Post vorstellen, welche statt der Klasse mysql in PHP die Klasse mysqli verwendet und so durch Prepared Statements SQL-Injections verhindert.
Die PHP Skripte sehen nun so aus:

register.php (http://bloggeroliver.bplaced.net/Chat/Secure/register.php):

<?php
include("connect.php");

$username = $_POST["username"];
$password = $_POST["password"];
$hashedpw = md5($password);

$stmt = $conn->prepare("SELECT username FROM Users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();

$stmt->store_result();
$num_rows = $stmt->num_rows;

if ($num_rows > 0) {
     echo "Existing";
}
else {
     $stmt = $conn->prepare("INSERT INTO Users (username, password) VALUES (?, ?);");
     $stmt->bind_param("ss", $username, $hashedpw);
     $stmt->execute();
     echo "Success";
}
?>

login.php (http://bloggeroliver.bplaced.net/Chat/Secure/login.php):

<?php
session_start();

include("connect.php");

$username = $_POST["username"];
$password = $_POST["password"];
$hashedpw = md5($password);

$stmt = $conn->prepare("SELECT password FROM Users WHERE username = ? LIMIT 1");
$stmt->bind_param("s", $username);
$stmt->execute();

$stmt->bind_result($rowpassword);
$row = $stmt->fetch();

if($rowpassword == $hashedpw) {
    $_SESSION["username"] = $username;
    echo "LoginGood";
}
else {
    echo "LoginBad";
}
?>

send.php (http://bloggeroliver.bplaced.net/Chat/Secure/send.php):

<?php
session_start();

include("connect.php");

$Recipient = $_POST["Recipient"];
$Message = $_POST["Message"];
$Sender = $_SESSION['username'];

if(!isset($_SESSION['username'])) {
     echo "Login first.";
     exit;
}  

$stmt = $conn->prepare("INSERT INTO Messages () VALUES (?, ?, ?);");
$stmt->bind_param("sss", $Sender, $Recipient, $Message);
$stmt->execute();
    
?>

receive.php (http://bloggeroliver.bplaced.net/Chat/Secure/receive.php):

<?php

session_start();

include("connect.php");

if(!isset($_SESSION['username']))
   {
   echo "Bitte erst login";
   exit;
   }   
    
$Recipient = $_SESSION['username'];

$stmt = $conn->prepare("SELECT Sender, Message FROM Messages WHERE Recipient = ?");
$stmt->bind_param("s", $Recipient);
$stmt->execute();
    
$stmt->bind_result($sender, $message);
while($row = $stmt->fetch())
   {
          echo "$sender<br />";
          echo "$message<br />";
          $stmt->bind_result($sender, $message);
   }

$stmt = $conn->prepare("DELETE FROM Messages WHERE Recipient = ?");
$stmt->bind_param("s", $Recipient);
$stmt->execute();
?>

Im Code des C# Clients ist nur eine Zeile zu ändern, und zwar die Variable ServerUrl auf die Adresse des neuen Skriptordners: string ServerUrl = "http://bloggeroliver.bplaced.net/Chat/Secure/";

Der komplette Quellcode kann hier heruntergeladen werden, das fertige Chat Programm eingestellt auf meinen Server kann über diesen Link installiert werden.
Auch in diesem Chat heiße ich bloggeroliver und freue mich auf eure Nachrichten!

Nun möchte ich hier noch generell die Sicherheit dieses Chats diskuttieren. Im Wesentlichen sehe ich noch 2 Sicherheitslücken:

  • Das Passwort beim Login wird unverschlüsselt übertragen. Ein Angreifer, welcher Zugriff auf den Netzverkehr hat, könnte dieses auslesen oder sich einfach durch Schicken der gleichen Nachricht selber anmelden. Dieses Problem werde ich im nächsten Post angehen. Eine Möglichkeit wäre, die Verbindung z.B. durch TLS/SSL zu verschlüsseln. Dafür ist aber ein (eventuell kostenpflichtiges) Zertifikat nötig, ich möchte es gerne mit "C# Bordmitteln" lösen. Daher werde ich eine Challenge / Response Authentifizierung mit einem Public Key implementieren.
  • Außerdem werden die Nachrichten unverschlüsselt übertragen, es tritt das gleiche Problem wie oben auf, jeder Angreifer mit Zugriff auf den Netzverkehr könnte also mitlesen. Außerdem kann der Datenbankbetreiber alles lesen. Allerdings würde ich dieses nicht unbedingt als Sicherheitsproblem ansehen (und auch als wesentlich weniger gravierend als der erste Punkt), sondern als Feature. Falls keine Verschlüsselung der Nachrichten gewünscht ist, kann man dies so lassen (es ist nicht unbedingt die Sicherheit des Systems gefährdet, nur keine Vetraulichkeit gewährleistet), und ansonsten eine Verschlüsselung einsetzen (was aber natürlich empfohlen ist). Im übernächsten Post werde ich auch dieses mit einem Public Key Verfahren lösen.

Samstag, 21. März 2015

C# Chat Client v1

Im heutigen Post möchte ich alle vorigen Posts verbinden um eine erste Version meines Chat Clients vorzustellen. Der tatsächliche Chat Client ist in C# geschrieben und sieht so aus:


















Es gibt einen zentralen Server, auf welchem PHP Skripte verfügbar sind. Diese dienen der Benutzerverwaltung (Registrierung / Login), und der Nachrichtenverwaltung. Alle Daten werden in einer MySQL Datenbank auf dem Server gespeichert. Es gibt also eine Tabelle mit den Benutzern sowie eine mit den Nachrichten. Beim Schicken einer Nachricht wird das entsprechende PHP Skript vom Client aufgerufen, dieses schreibt die Nachricht dann in die Datenbank hinein. Der Client des Empfängers ruft periodisch ein Skript zur Nachrichtenabholung auf, dieses leitet ihm dann eventuelle Nachrichten weiter.

Es gibt im wesentlichen 2 Klassen: Die Klasse Form1 zur Verwaltung der grafischen Oberfläche, sowie die Klasse SimpleChatClient, welche den Kern des Chat Clients darstellt und die Kommunikation mit dem Server erledigt. 
Das Design der Oberfläche ist das gleiche wie beim erweiterten Facebook Chat, ich möchte auf den Code der Klasse hier nicht eingehen, sondern mich auf den Kern des Chats konzentrieren.

In der Klasse SimpleChatClient ist die Funktion HTTPPost() von zentraler Bedeutung. Sie erzeugt HTTP POST Requests mit der HttpWebRequest Klasse und benutzt außerdem Cookies, sodass wir PHP Sessions benutzen können. Die gewünschte Adresse und die Parameter übergeben wir an die Funktion. Die Funktionen Register(), Login(), Send() und Receive() haben die zu erwartenden Funktionen und rufen HTTPPost() mit den passenden Argumenten auf. Die Funktionen sind sehr einfach, sie leiten im wesentlichen nur die Anfrage an die passenden PHP Skripte weiter und die (eventuellen) Ausgaben an die Chat Oberfläche. Die Funktionen Send() und Receive() sollen natürlich nur den jeweiligen Benutzern zur Verfügung stehen, deshalb prüfen die dazu gehörigen PHP Skripte, ob der aktuelle Benutzer bereits eine Session gestartet hat. Die globale Variable Cookie speichert den Cookie für den HTTP Request, welcher beim Einloggen zurückgegeben wird.

Kommen wir nun zum PHP Code auf dem Server. Alle PHP Skripte haben am Anfang die Codezeile include("connect.php");, welche das Skript connect.php einbindet. Dieses bereitet lediglich die MySQL Verbindung durch $conn = new mysqli(server, username, password, database); vor. Die Daten dazu werde ich hier nicht veröffentlichen, da ich möchte, dass der Client auch (sicher) benutzt werden kann.
Natürlich kann jeder seinen eigenen Server erstellen und muss dafür nur das Skript connect.php ändern (und natürlich die Adressen im C# Code), in der Datenbank werden die folgenden Tabellen benötigt: Users, mit den Attributen username und password, und Messages mit Sender, Recipient und Message.
Ich möchte der Einfachheit halber wieder zuerst PHP Code mit der Klasse mysql zeigen, im nächsten Post dann die sichere Version mit mysqli.
Die Dateien register.php und login.php sind die im Post über ein Loginsystem vorgestellten:

register.php (http://bloggeroliver.bplaced.net/Chat/Insecure/register.php):

<?php
include("connect.php");

$username = $_POST["username"];
$password = $_POST["password"];
$hashedpw = md5($password);

$checkuser = mysql_query("SELECT username FROM Users WHERE username = '$username'");
$num_rows = mysql_num_rows($checkuser);

if ($num_rows > 0) {
     echo "Existing";
}
else {
     mysql_query("INSERT INTO Users (username, password) VALUES ('$username', '$hashedpw');");
     echo "Success";
}
?>

login.php (http://bloggeroliver.bplaced.net/Chat/Insecure/login.php):

<?php
session_start();

include("connect.php");

$username = $_POST["username"];
$password = $_POST["password"];
$hashedpw = md5($password);

$result = mysql_query("SELECT username, password FROM Users WHERE username = '$username' LIMIT 1");
$row = mysql_fetch_object($result);
    
if($row->password == $hashedpw) {
    $_SESSION["username"] = $username;
    echo "LoginGood";
}
else {
    echo "LoginBad";
}
?>

In der Datei send.php (http://bloggeroliver.bplaced.net/Chat/Insecure/send.php) wird zuerst auf eine gültige Session geprüft, und falls diese existiert wird die übergebene Nachricht in die Datenbank geschrieben, wobei der angemeldete Nutzer als Absender eingetragen wird:

<?php
session_start();

include("connect.php");

$Recipient = $_POST["Recipient"];
$Message = $_POST["Message"];
$Sender = $_SESSION['username'];

if(!isset($_SESSION['username'])) {
     echo "Login first.";
     exit;
}

mysql_query("INSERT INTO Messages () VALUES ('$Sender', '$Recipient', '$Message');");
?>

In receive.php (http://bloggeroliver.bplaced.net/Chat/Insecure/receive.php) wird ebenfalls zuerst auf eine gültige Session geprüft, und dann werden alle Nachrichten mit dem aktuellen Nutzer als Empfänger aus der Datenbank gelesen, zurückgegeben und anschließend gelöscht:

<?php

session_start();

include("connect.php");

if(!isset($_SESSION['username']))
   {
   echo "Bitte erst login";
   exit;
   }
    
$Recipient = $_SESSION['username'];

$eintrag = "SELECT Sender, Message FROM Messages WHERE Recipient = '$Recipient'";
$eintragen = mysql_query($eintrag);
while($row = mysql_fetch_object($eintragen))
   {
          echo "$row->Sender<br />";
          echo "$row->Message<br />";
   }
$delete  = "DELETE FROM Messages WHERE Recipient = '$Recipient'";
$eintragen2 = mysql_query($delete);
?>

Nun der Code des C# Clients:

Form1.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.Net;
using System.IO;

namespace SimpleChatClient
{
    public class SimpleChatClient
    {
        public class User
        {
            public string Username;

            public User(string username)
            {
                Username = username;
            }
        }

        public class Message
        {
            public string Sender;
            public string Msg;

            public Message(string sender, string message)
            {
                Sender = sender;
                Msg = message;
            }
        }

        User CurrentUser = null;
        CookieContainer Cookie = null;
        string ServerUrl = "http://bloggeroliver.bplaced.net/Chat/Insecure/";

        public string GetUsername()
        {
            return CurrentUser.Username;
        }

        private string HTTPPost(string url, string postparams)
        {
            string responseString = "";

            try
            {
                // performs the desired http post request for the url and parameters
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                request.CookieContainer = Cookie; // explicitely use the cookiecontainer to save the session

                string postData = postparams;
                byte[] data = Encoding.UTF8.GetBytes(postData);

                request.Method = "POST";
                request.ContentType = "application/x-www-form-urlencoded; charset=utf-8";
                request.ContentLength = data.Length;

                using (var stream = request.GetRequestStream())
                {
                    stream.Write(data, 0, data.Length);
                }

                var response = (HttpWebResponse)request.GetResponse();

                responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();

                return responseString;
            }
            catch
            {
                // MessageBox.Show("error" + url + postparams + responseString);
                return null;
            }
        }

        public string Register(string username, string password)
        {
            // register a new user
            return HTTPPost(ServerUrl + "register.php", "username=" + username + "&password=" + password);
        }

        public bool Login(string username, string password)
        {
            // login
            Cookie = new CookieContainer();
            string Login = HTTPPost(ServerUrl + "login.php", "username=" + username + "&password=" + password);
            if (Login == "LoginGood")
            {
                CurrentUser = new User(username);
                return true;
            }
            else
            {
                Cookie = null;
                return false;
            }
        }

        public void Logout()
        {
            // logout
            CurrentUser = null;
            Cookie = null;
        }

        public string Send(string recipient, string message)
        {
            // send
            return (HTTPPost(ServerUrl + "send.php", "Recipient=" + recipient + "&Message=" + message));
        }

        public List<Message> Receive()
        {
            // receive messages
            if (CurrentUser == null)
                return new List<Message>();
            string Messages = HTTPPost(ServerUrl + "receive.php", "");
            if (Messages == null)
                return new List<Message>();

            // message format is: sender<br />message<br />, split regarding to this in single messages
            string[] Splits = Messages.Split(new string[] { "<br />" }, StringSplitOptions.None);
            List<Message> Received = new List<Message>();
            for (int i = 0; i < Splits.Length - 1; i += 2)
            {
                Received.Add(new Message(Splits[i], Splits[i + 1]));
            }
            return Received;
        }
    }

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        SimpleChatClient Client;
        bool LoggedIn = false;
        Dictionary<string, Chat> Chats = new Dictionary<string, Chat>(); // stores all active chats

        private void button1_Click(object sender, EventArgs e)
        {
            if (!LoggedIn)
            {
                Client = new SimpleChatClient();

                bool Login = (Client.Login(textBox1.Text, textBox2.Text));
                if (Login)
                {
                    // Login successful, enable chatting etc.
                    textBox3.Enabled = true;
                    button3.Enabled = true;
                    this.Height = 570;
                    tabControl1.Visible = true;
                    LoggedIn = true;
                    button1.Text = "Logout";
                    button2.Enabled = false;
                }
                else
                    MessageBox.Show("Login failed.");
            }
            else
            {
                // logout, disable chatting etc.
                Client = new SimpleChatClient();
                Chats = new Dictionary<string, Chat>();
                tabControl1.TabPages.Clear();
                textBox1.Text = "";
                textBox2.Text = "";
                button2.Enabled = true;
                textBox3.Enabled = false;
                button3.Enabled = false;
                this.Height = 176;
                tabControl1.Visible = false;
                LoggedIn = false;
                button1.Text = "Login";
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            Client = new SimpleChatClient();
            string Register = Client.Register(textBox1.Text, textBox2.Text);
            if (Register == "Success")
            {
                MessageBox.Show("Account created.");
            }
            if (Register == "Existing")
            {
                MessageBox.Show("Username already in use.");
            }
        }

        private void button3_Click_1(object sender, EventArgs e)
        {
            ProvideChat(textBox3.Text);
        }

        private void ProvideChat(string name)
        {
            Chat Dummy;
            bool ChatExisting = Chats.TryGetValue(name, out Dummy);
            if (ChatExisting)
            {   // if chat already exists, make its tabpage active
                tabControl1.SelectedTab = Dummy.ChatPage;
                return;
            }
            else
            {
                // create new tabpage for the conversation
                TabPage NewPage = new TabPage(name);

                TextBox ChatWindow = new TextBox();
                ChatWindow.Left = 10;
                ChatWindow.Top = 10;
                ChatWindow.Width = 532;
                ChatWindow.Height = 180;
                ChatWindow.Multiline = true;
                ChatWindow.ScrollBars = ScrollBars.Vertical;
                ChatWindow.ReadOnly = false;
                NewPage.Controls.Add(ChatWindow);

                TextBox SendBox = new TextBox();
                SendBox.Left = 10;
                SendBox.Top = 200;
                SendBox.Width = 450;
                NewPage.Controls.Add(SendBox);
                SendBox.Name = "snd" + name;
                SendBox.Click += new EventHandler(SendBox_Click);
                SendBox.TextChanged += new EventHandler(SendBox_TextChanged);

                Button SendButton = new Button();
                SendButton.Left = 470;
                SendButton.Top = 200;
                SendButton.Text = "Send";
                SendButton.Name = "btn" + name;
                SendButton.Click += new EventHandler(SendButton_Click);
                NewPage.Controls.Add(SendButton);

                Chat NewChat = new Chat();
                NewChat.ChatWindow = ChatWindow;
                NewChat.SendBox = SendBox;
                NewChat.ChatPage = NewPage;
                NewChat.Partner = name;
                NewChat.SendButton = SendButton;

                NewPage.Name = "tpg" + name;
                tabControl1.SelectedIndexChanged += new EventHandler(tabControl1_SelectedIndexChanged);

                Chats.Add(name, NewChat);

                this.AcceptButton = NewChat.SendButton;

                if (tabControl1.InvokeRequired)
                {
                    tabControl1.Invoke(new Action(() =>
                    {
                        tabControl1.TabPages.Add(NewPage);
                        tabControl1.SelectedTab = NewPage;
                    }));
                }
                else
                {
                    tabControl1.TabPages.Add(NewPage);
                    tabControl1.SelectedTab = NewPage;
                }

                this.ActiveControl = NewChat.SendBox;
            }
        }

        private void tabControl1_SelectedIndexChanged(Object sender, EventArgs e)
        {
            // change tabpages / chats
            if (((TabControl)sender).SelectedIndex != -1)
            {
                string Receiver = ((TabControl)sender).TabPages[((TabControl)sender).SelectedIndex].Name.ToString().Substring(3, ((TabControl)sender).TabPages[((TabControl)sender).SelectedIndex].Name.ToString().Length - 3);
                this.AcceptButton = Chats[Receiver].SendButton;
                this.ActiveControl = Chats[Receiver].SendBox;
            }
        }

        private void SendBox_Click(Object sender, EventArgs e)
        {
            // click in textbox for sending
            string Receiver = ((TextBox)sender).Name.Substring(3, ((TextBox)sender).Name.Length - 3);
            Chat CurrentChat = Chats[Receiver];
            CurrentChat.NewMessage = false;
            CurrentChat.ChatPage.Text = CurrentChat.Partner;
        }

        private void SendBox_TextChanged(Object sender, EventArgs e)
        {
            string Receiver = ((TextBox)sender).Name.Substring(3, ((TextBox)sender).Name.Length - 3);
            Chat CurrentChat = Chats[Receiver];
            CurrentChat.NewMessage = false;
            CurrentChat.ChatPage.Text = CurrentChat.Partner;
        }

        private void SendButton_Click(Object sender, EventArgs e)
        {
            // send message
            string Receiver = ((Button)sender).Name.Substring(3, ((Button)sender).Name.Length - 3);
            Chat CurrentChat = Chats[Receiver];
            Client.Send(Receiver, CurrentChat.SendBox.Text);
            CurrentChat.ChatWindow.Text += Client.GetUsername() + ": " + CurrentChat.SendBox.Text + Environment.NewLine;
            CurrentChat.SendBox.Text = "";
            Chats[Receiver].ChatWindow.SelectionStart = Chats[Receiver].ChatWindow.TextLength;
            Chats[Receiver].ChatWindow.ScrollToCaret();
        }

        private void IncomingMessage(string NameSender, string message)
        {
            // rceive an incoming message and display it in the correct chat window
            ProvideChat(NameSender);

            Chats[NameSender].NewMessage = true;
            Chats[NameSender].ChatWindow.Invoke(new Action(() =>
            {
                Chats[NameSender].ChatWindow.Text += NameSender + ": " + message + Environment.NewLine;
                Chats[NameSender].ChatWindow.SelectionStart = Chats[NameSender].ChatWindow.TextLength;
                Chats[NameSender].ChatWindow.ScrollToCaret();
            }));
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            // periodically poll messages from server
            if (Client != null)
            {
                List<SimpleChatClient.Message> Messages = Client.Receive();
                foreach (SimpleChatClient.Message m in Messages)
                {
                    IncomingMessage(m.Sender, m.Msg);
                }
            }
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            this.Height = 176;
        }

        private void timer2_Tick(object sender, EventArgs e)
        {
            // timer used for the blinking effect for new messages
            foreach (Chat c in Chats.Values)
            {
                if (c.NewMessage)
                {
                    string BlankName = "";
                    BlankName = BlankName.PadLeft(c.Partner.Length, ' ');
                    if (c.ChatPage.Text == BlankName)
                        c.ChatPage.Text = c.Partner;
                    else
                        c.ChatPage.Text = BlankName;
                }
            }
        }

        private void button4_Click(object sender, EventArgs e)
        {
            if (tabControl1.SelectedIndex != -1)
            {
                string Current = tabControl1.TabPages[tabControl1.SelectedIndex].Name.ToString().Substring(3, tabControl1.TabPages[tabControl1.SelectedIndex].Name.ToString().Length - 3);
                tabControl1.TabPages.RemoveAt(tabControl1.SelectedIndex);
                Chats.Remove(Current);
            }
        }
    }

    public class Chat
    {
        public TextBox ChatWindow;
        public TextBox SendBox;
        public bool NewMessage = false;
        public TabPage ChatPage;
        public string Partner;
        public Button SendButton;
    }
  
}

Form1.Designer.cs:

namespace SimpleChatClient
{
    partial class Form1
    {
        /// <summary>
       /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
       /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
       /// Required method for Designer support - do not modify
       /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.button2 = new System.Windows.Forms.Button();
            this.button1 = new System.Windows.Forms.Button();
            this.label2 = new System.Windows.Forms.Label();
            this.textBox2 = new System.Windows.Forms.TextBox();
            this.label1 = new System.Windows.Forms.Label();
            this.textBox1 = new System.Windows.Forms.TextBox();
            this.tabControl1 = new System.Windows.Forms.TabControl();
            this.button3 = new System.Windows.Forms.Button();
            this.textBox3 = new System.Windows.Forms.TextBox();
            this.timer1 = new System.Windows.Forms.Timer(this.components);
            this.timer2 = new System.Windows.Forms.Timer(this.components);
            this.button4 = new System.Windows.Forms.Button();
            this.SuspendLayout();
            //
            // button2
            //
            this.button2.Location = new System.Drawing.Point(125, 86);
            this.button2.Name = "button2";
            this.button2.Size = new System.Drawing.Size(80, 25);
            this.button2.TabIndex = 11;
            this.button2.Text = "Register";
            this.button2.UseVisualStyleBackColor = true;
            this.button2.Click += new System.EventHandler(this.button2_Click);
            //
            // button1
            //
            this.button1.Location = new System.Drawing.Point(25, 86);
            this.button1.Name = "button1";
            this.button1.Size = new System.Drawing.Size(81, 25);
            this.button1.TabIndex = 10;
            this.button1.Text = "Login";
            this.button1.UseVisualStyleBackColor = true;
            this.button1.Click += new System.EventHandler(this.button1_Click);
            //
            // label2
            //
            this.label2.AutoSize = true;
            this.label2.Location = new System.Drawing.Point(271, 49);
            this.label2.Name = "label2";
            this.label2.Size = new System.Drawing.Size(53, 13);
            this.label2.TabIndex = 9;
            this.label2.Text = "Password";
            //
            // textBox2
            //
            this.textBox2.Location = new System.Drawing.Point(25, 49);
            this.textBox2.Name = "textBox2";
            this.textBox2.PasswordChar = '*';
            this.textBox2.Size = new System.Drawing.Size(219, 20);
            this.textBox2.TabIndex = 8;
            //
            // label1
            //
            this.label1.AutoSize = true;
            this.label1.Location = new System.Drawing.Point(271, 23);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(55, 13);
            this.label1.TabIndex = 7;
            this.label1.Text = "Username";
            //
            // textBox1
            //
            this.textBox1.Location = new System.Drawing.Point(25, 23);
            this.textBox1.Name = "textBox1";
            this.textBox1.Size = new System.Drawing.Size(219, 20);
            this.textBox1.TabIndex = 6;
            //
            // tabControl1
            //
            this.tabControl1.Dock = System.Windows.Forms.DockStyle.Bottom;
            this.tabControl1.Location = new System.Drawing.Point(0, 199);
            this.tabControl1.Name = "tabControl1";
            this.tabControl1.SelectedIndex = 0;
            this.tabControl1.Size = new System.Drawing.Size(804, 329);
            this.tabControl1.TabIndex = 12;
            this.tabControl1.Visible = false;
            //
            // button3
            //
            this.button3.Enabled = false;
            this.button3.Location = new System.Drawing.Point(274, 150);
            this.button3.Name = "button3";
            this.button3.Size = new System.Drawing.Size(77, 32);
            this.button3.TabIndex = 13;
            this.button3.Text = "Start Chat";
            this.button3.UseVisualStyleBackColor = true;
            this.button3.Click += new System.EventHandler(this.button3_Click_1);
            //
            // textBox3
            //
            this.textBox3.Enabled = false;
            this.textBox3.Location = new System.Drawing.Point(25, 157);
            this.textBox3.Name = "textBox3";
            this.textBox3.Size = new System.Drawing.Size(219, 20);
            this.textBox3.TabIndex = 14;
            //
            // timer1
            //
            this.timer1.Enabled = true;
            this.timer1.Interval = 1000;
            this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
            //
            // timer2
            //
            this.timer2.Enabled = true;
            this.timer2.Interval = 1000;
            this.timer2.Tick += new System.EventHandler(this.timer2_Tick);
            //
            // button4
            //
            this.button4.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
            this.button4.Location = new System.Drawing.Point(751, 157);
            this.button4.Name = "button4";
            this.button4.Size = new System.Drawing.Size(41, 26);
            this.button4.TabIndex = 15;
            this.button4.Text = "X";
            this.button4.UseVisualStyleBackColor = true;
            this.button4.Click += new System.EventHandler(this.button4_Click);
            //
            // Form1
            //
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(804, 528);
            this.Controls.Add(this.button4);
            this.Controls.Add(this.textBox3);
            this.Controls.Add(this.button3);
            this.Controls.Add(this.button2);
            this.Controls.Add(this.button1);
            this.Controls.Add(this.label2);
            this.Controls.Add(this.textBox2);
            this.Controls.Add(this.label1);
            this.Controls.Add(this.textBox1);
            this.Controls.Add(this.tabControl1);
            this.Name = "Form1";
            this.Text = "SimpleChat";
            this.Load += new System.EventHandler(this.Form1_Load);
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private System.Windows.Forms.Button button2;
        private System.Windows.Forms.Button button1;
        private System.Windows.Forms.Label label2;
        private System.Windows.Forms.TextBox textBox2;
        private System.Windows.Forms.Label label1;
        private System.Windows.Forms.TextBox textBox1;
        private System.Windows.Forms.TabControl tabControl1;
        private System.Windows.Forms.Button button3;
        private System.Windows.Forms.TextBox textBox3;
        private System.Windows.Forms.Timer timer1;
        private System.Windows.Forms.Timer timer2;
        private System.Windows.Forms.Button button4;
    }
}

Das komplette Projekt kann hier heruntergeladen werden, hier gibt es einen Link zu den Installationsdateien (durch Ausführen der Datei setup.exe wird das Programm installiert).
Ich heiße in dem Chat auch bloggeroliver (Groß- / Kleinschreibung ist wichtig) und freue mich auf eure Nachrichten!

Im nächsten Post wird, wie gesagt, das Problem der MySQL Injections angegangen, dort werde ich außerdem andere Sicherheitslücken dieses Chats beschreiben und deren Lösung zeigen.

Freitag, 20. März 2015

(Sicheres) PHP Loginsystem (geschützt gegen SQL-Injections)

In einem vorigen Post habe ich ein Loginsystem mit PHP vorgestellt. Dieses war durch die Verwendung der einfachen Queries aus der mysql Klasse anfällig gegen SQL-Injections. Diese Lücke möchte ich nun heute schließen, in dem ich die im vorigen Post vorgestellte Klasse mysqli und Prepared Statements verwende.
Da das Prinzip des Loginsystems gleich geblieben ist und die Prepared Statements im verlinkten Post erklärt wurden, hier einfach der Code:

register.php (http://bloggeroliver.bplaced.net/PHPExamples/LoginV2/register.php):

<?php
$conn = new mysqli("db4free.net", "csharptricks", "12345678", "csharptricks");

$username = $_POST["username"];
$password = $_POST["password"];
$hashedpw = md5($password);

$stmt = $conn->prepare("SELECT username FROM Users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();

if ($stmt->num_rows > 0) {
     echo "Existing";
}
else {
     $stmt = $conn->prepare("INSERT INTO Users (username, password) VALUES (?, ?);");
     $stmt->bind_param("ss", $username, $hashedpw);
     $stmt->execute();
     echo "Success";
}
?>

login.php (http://bloggeroliver.bplaced.net/PHPExamples/LoginV2/login.php):

<?php
session_start();

$conn = new mysqli("db4free.net", "csharptricks", "12345678", "csharptricks");

$username = $_POST["username"];
$password = $_POST["password"];
$hashedpw = md5($password);

$stmt = $conn->prepare("SELECT username, password FROM Users WHERE username = ? LIMIT 1");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($output1, $output2);
$row = $stmt->fetch();

if($output2 == $hashedpw) {
    $_SESSION["username"] = $username;
    echo "LoginGood";
}
else {
    echo "LoginBad";
}
?>

Da die Schnittstelle nach außen gleich geblieben ist, kann natürlich auch der C# Code für die Loginanwendung aus dem vorigen Post wieder verwendet werden.

Donnerstag, 19. März 2015

Sicherer Zugriff auf MySQL Datenbanken mit PHP

In einem vorigen Post habe ich erklärt, wie man mittels PHP auf eine MySQL Datenbank zugreift.
Dort habe ich eine einfache bzw. veraltete Variante zur Demonstration gewählt, nämlich die Klasse mysql. Mittlerweile sollte man die Klasse mysqli wählen, das i steht für improved.
Insbesondere möchte ich in diesem Post eine neue, wichtige Funktion von mysqli vorstellen, nämlich die Möglichkeit sogenannte Prepared Statements zu verwenden.
Die Methode aus dem vorigen Post zur Kommunikation mit der SQL Datenbank lässt nämlich eine gravierenden Attacke zu: Eine SQL-Injection ist möglich. Dieses bezeichnet das Einschleußen von Code auf dem Server. Warum? Nun, schauen wir uns die Kommunikation mit der Datenbank an: Wir haben die POST Parameter im Skript verwendet, um zum Beispiel den eingegebenen Benutzernamen abzufragen und in die Datenbank zu schreiben. Ein Angreifer könnte nun aber zum Beispiel in den Benutzernamen ein Semikolon, Anführungszeichen etc. einfügen, so die Abfrage schließen und im Benutzernamen weiter eine neue Abfrage / Anweisung stellen, welche dann auch ausgeführt würde, da für die Datenbank einfach 2 Abfragen ankommen!
Um dies zu verhindern, bzw. zu versuchen zu verhindern (100% Sicherheit kann nie garantiert werden), könnte man zum Beispiel versuchen, per Hand in den ausgeführten SQL Abfragen Sonderzeichen wie ; zu escapen und so die Attacke zu verhindern. Ich werde hier aber eine höhere Abstraktionsebene wählen und eine existierende Methode der Klasse mysqli verwenden, welche dies für uns erledigt. Das Stichwort sind Prepared Statements, die quasi eine Abfrage vorbereiten und die "sauberen" Parameter an diese binden. PreparedStatements bieten sehr guten Schutz gegen SQL-Injections.
Das Grundprinzip eines Prepared Statements ist das folgende:
Zuerst erstellen wir uns eine Verbindungsvariable:

$conn = new mysqli(server, username, password, database);

Dann benutzen wir diese zur Abfrage:

$stmt = $conn->prepare("query ... ?");
$stmt->bind_param("s...", params ...);
$stmt->execute();

In der 1. Zeile bereiten wir die Anfrage vor, in der Anfrage ersetzen wir dabei die Parameter durch ein ?. In der 2. Zeile binden wir die Parameter an diese Platzhalter, die erste Zeichenkette gibt den Typ an. s steht für String, i für Integer und d für Double. Die nächsten Argumente sind dann die Parameter. Schließlich führen wir die Anweisung aus.

Um eine Abfrage zu stellen und die Antwort auszulesen, führen wir auch zuerst die obigen 3 Zeilen aus. Die Antwort können wir dann mit folgender Struktur abgreifen:

$stmt->bind_result(params);
while($row = $stmt->fetch())
   {
echo params .... ;
$stmt->bind_result($sender, $message);
   }

Ich möchte nun das Skript des vorigen Posts zu MySQL mit mysqli umschreiben, ich denke, das Prinzip sollte dann verständlich sein:

<?php
$FirstName = $_POST["FirstName"];
$LastName = $_POST["LastName"];

$conn = new mysqli("db4free.net", "csharptricks", "12345678", "csharptricks");

$stmt = $conn->prepare("INSERT INTO Customers (FirstName, LastName) VALUES (?, ?);");
$stmt->bind_param("ss", $FirstName, $LastName);
$stmt->execute();
  
$stmt = $conn->prepare("SELECT FirstName, LastName FROM Customers;");
$stmt->execute();
$stmt->bind_result($output1, $output2);
while($row = $stmt->fetch())
   {
          echo "$output1<br />";
          echo "$output2<br />";
          $stmt->bind_result($output1, $output2);
   }
?>