Commit b4038162 authored by Håkon Gylterud's avatar Håkon Gylterud
Browse files

No git history for Inchat.

parents
# INCHAT – The INsecure CHAT application
Welcome to this second mandatory assignment of INF226.
In this assignment you will be analysing the security
of a program called inChat – a very simple chat application,
in the shape of a [Jetty](https://www.eclipse.org/jetty/)
web application.
inChat has been especially crafted to contain a number
of security flaws. You can imagine that it has been
programmed by a less competent collegue, and that after
numerous securiy incidents, your organisation has decided
that you – a competent security professional – should take
some time to secure the app.
## Getting and building the project
Log into [`git.app.uib.no`](https://git.app.uib.no/Hakon.Gylterud/inf226-2020-inchat) and make your
own fork of the project there. *Make sure your fork is private!*
You can then clone your repo to your own machine.
To build the project you can use Maven on the command line, or configure
your IDE to use Maven to build the project.
- `mvn compile` builds the project
- `mvn test` runs the tests. (There are only a few unit test – feel free to add more).
- `mvn exec:java` runs the web app.
Once the web-app is running, you can access it on [`localhost:8080`](http://localhost:8080/).
## Updates
Most likely the source code of the project will be updated by Håkon
while you are working on it. Therefore, it will be part of
your assignment to merge any new commits into your own branch.
## Improvements?
Have you found a non-security related bug?
Feel free to open an issue on the project GitLab page.
The best way is to make a separate `git branch` for these
changes, which do not contain your sulutions.
(This is ofcourse completely volountary – and not a graded
part of the assignment)
If you want to add your own features to the chat app - feel free
to do so! If you want to share them, contact Håkon and we can
incorporate them into the main repo.
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<link rel="stylesheet" href="style.css">
<title>Log in to Inforum</title>
</head>
<body>
<h1 class="topic">InChat</h1>
<div class="actionbar">
<a class="action" href="/register">Register</a>
<a class="action" href="/login">Login</a>
</div>
<p>Welcome to InChat, the insecure chatting platform!</p>
</body>
</html>
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<link rel="stylesheet" href="style.css">
<title>Log in to Inforum</title>
</head>
<body>
<h1 class="topic">Login to inforum</h1>
<form class="login" action="/" method="post">
<div class="username"><input type="text" name="username" placeholder="Username"></div>
<div class="password"><input type="password" name="password" placeholder="Password"></div>
<div class="loginbutton"><input type="submit" name="login" value="Login"></div>
</form>
<p>No account? <a href="/register">Register here!</a></p>
</body>
</html>
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<link rel="stylesheet" href="style.css">
<title>Log in to Inforum</title>
</head>
<body>
<h1 class="topic">InChat</h1>
<div class="actionbar">
<a class="action" href="/join">Join a channel!</a>
<a class="action" href="/logout">Logout</a>
</div>
<div class="main">
<ul class="chanlist">
<li>#Foochan</li>
<li><b>#Barchan</b></li>
</ul>
<main role="main" class="channel">
<div class="entry">
<div class="user">Joe</div>
<div class="text"><p>I started this thread in order to test the
layout of this web page.</p>
<p>I expect to write a lot here later – especially about lorem ipsum.
But for the moment I will content myself with a few lines.</p>
</div>
<div class="controls">
<a class="action" href="/delete">Delete</a>
<a class="action" href="/edit">Edit</a>
</div>
</div>
<div class="entry">
<div class="user">Delia</div>
<div class="text"><p>Hey, Joe!</p>
</div>
<div class="controls">
<a class="action" href="/delete">Delete</a>
<a class="action" href="/edit">Edit</a>
<div>
</div>
</main>
<aside class="chanmenu">
Here comes the menu.
</aside>
</div>
</body>
</html>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>inf226</groupId>
<artifactId>inchat</artifactId>
<version>0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>inChat</name>
<properties>
<jettyVersion>9.4.9.v20180320</jettyVersion>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jettyVersion}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.28.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.lambdaworks/scrypt -->
<dependency>
<groupId>com.lambdaworks</groupId>
<artifactId>scrypt</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<executions>
<execution><goals><goal>java</goal></goals></execution>
</executions>
<configuration>
<mainClass>inf226.inchat.Handler</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<compilerArgs>
<arg>-Xlint:all,-options,-path</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
</plugins>
</build>
</project>
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<link rel="stylesheet" href="style.css">
<title>Register an account on InChat</title>
</head>
<body>
<h1 class="topic">Register for InChat</h1>
<form class="register" action="/" method="post">
<p>Chose a username and a password:</p>
<div class="username"><input type="text" name="username" placeholder="Username"></div>
<div class="password"><input type="password" name="password" placeholder="Password"></div>
<div class="password"><input type="password" name="password_repeat" placeholder="Retype password"></div>
<div class="button"><input type="submit" name="register" value="Register"></div>
</form>
</body>
</html>
async function subscribe(id,vers) {
let response = await fetch("/subscribe/" + id +"?version=" + vers);
if (response.status == 502) {
// Status 502 is a connection timeout error,
// may happen when the connection was pending for too long,
// and the remote server or a proxy closed it
// let's reconnect
await subscribe();
} else if (response.status != 200) {
// An error - let's show it
alert(response.statusText);
// Reconnect in one second
await new Promise(resolve => setTimeout(resolve, 1000));
await subscribe();
} else {
// Get and show the message
let message = await response.text();
let lineend = message.indexOf("\n")
let newvers = message.substr(0, lineend);
let html = message.substr(lineend+1);
let chan = document.getElementById("channel");
let chanevents = document.getElementById("chanevents");
chan.replaceChild(htmlToElem(html),chanevents);
// Call subscribe() again to get the next message
await subscribe(id,newvers);
}
}
function htmlToElem(html) {
let temp = document.createElement('template');
html = html.trim(); // Never return a space text node as a result
temp.innerHTML = html;
return temp.content.firstChild;
}
function submitOnEnter(event){
if(event.which === 13 && !event.shiftKey){
event.target.form.submit();
//event.target.form.dispatchEvent(new Event("submit", {cancelable: true}));
event.preventDefault();
}
}
package inf226.inchat;
import inf226.util.immutable.List;
import inf226.util.Pair;
import inf226.storage.*;
/**
* The Account class holds all information private to
* a specific user.
**/
public final class Account {
/*
* A channel consists of a User object of public account info,
* and a list of channels which the user can post to.
*/
public final Stored<User> user;
public final List<Pair<String,Stored<Channel>>> channels;
public final String password;
public Account(final Stored<User> user,
final List<Pair<String,Stored<Channel>>> channels,
final String password) {
this.user = user;
this.channels = channels;
this.password = password;
}
/**
* Create a new Account.
*
* @param user The public User profile for this user.
* @param password The login password for this account.
**/
public static Account create(final Stored<User> user,
final String password) {
return new Account(user,List.empty(), password);
}
/**
* Join a channel with this account.
*
* @return A new account object with the cannnel added.
*/
public Account joinChannel(final String alias,
final Stored<Channel> channel) {
Pair<String,Stored<Channel>> entry
= new Pair<String,Stored<Channel>>(alias,channel);
return new Account
(user,
List.cons(entry,
channels),
password);
}
/**
* Check weather if a string is a correct password for
* this account.
*
* @return true if password matches.
*/
public boolean checkPassword(String password) {
return this.password.equals(password);
}
}
package inf226.inchat;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.Instant;
import java.util.UUID;
import inf226.storage.*;
import inf226.util.immutable.List;
import inf226.util.*;
/**
* This class stores accounts in the database.
*/
public final class AccountStorage
implements Storage<Account,SQLException> {
final Connection connection;
final Storage<User,SQLException> userStore;
final Storage<Channel,SQLException> channelStore;
/**
* Create a new account storage.
*
* @param connection The connection to the SQL database.
* @param userStore The storage for User data.
* @param channelStore The storage for channels.
*/
public AccountStorage(Connection connection,
Storage<User,SQLException> userStore,
Storage<Channel,SQLException> channelStore)
throws SQLException {
this.connection = connection;
this.userStore = userStore;
this.channelStore = channelStore;
connection.createStatement()
.executeUpdate("CREATE TABLE IF NOT EXISTS Account (id TEXT PRIMARY KEY, version TEXT, user TEXT, password TEXT, FOREIGN KEY(user) REFERENCES User(id) ON DELETE CASCADE)");
connection.createStatement()
.executeUpdate("CREATE TABLE IF NOT EXISTS AccountChannel (account TEXT, channel TEXT, alias TEXT, ordinal INTEGER, PRIMARY KEY(account,channel), FOREIGN KEY(account) REFERENCES Account(id) ON DELETE CASCADE, FOREIGN KEY(channel) REFERENCES Channel(id) ON DELETE CASCADE)");
}
@Override
public Stored<Account> save(Account account)
throws SQLException {
final Stored<Account> stored = new Stored<Account>(account);
String sql =
"INSERT INTO Account VALUES('" + stored.identity + "','"
+ stored.version + "','"
+ account.user.identity + "','"
+ account.password + "')";
connection.createStatement().executeUpdate(sql);
// Write the list of channels
final Maybe.Builder<SQLException> exception = Maybe.builder();
final Mutable<Integer> ordinal = new Mutable<Integer>(0);
account.channels.forEach(element -> {
String alias = element.first;
Stored<Channel> channel = element.second;
final String msql
= "INSERT INTO AccountChannel VALUES('" + stored.identity + "','"
+ channel.identity + "','"
+ alias + "','"
+ ordinal.get().toString() + "')";
try { connection.createStatement().executeUpdate(msql); }
catch (SQLException e) { exception.accept(e) ; }
ordinal.accept(ordinal.get() + 1);
});
Util.throwMaybe(exception.getMaybe());
return stored;
}
@Override
public synchronized Stored<Account> update(Stored<Account> account,
Account new_account)
throws UpdatedException,
DeletedException,
SQLException {
final Stored<Account> current = get(account.identity);
final Stored<Account> updated = current.newVersion(new_account);
if(current.version.equals(account.version)) {
String sql = "UPDATE Account SET" +
" (version,user) =('"
+ updated.version + "','"
+ new_account.user.identity
+ "') WHERE id='"+ updated.identity + "'";
connection.createStatement().executeUpdate(sql);
// Rewrite the list of channels
connection.createStatement().executeUpdate("DELETE FROM AccountChannel WHERE account='" + account.identity + "'");
final Maybe.Builder<SQLException> exception = Maybe.builder();
final Mutable<Integer> ordinal = new Mutable<Integer>(0);
new_account.channels.forEach(element -> {
String alias = element.first;
Stored<Channel> channel = element.second;
final String msql
= "INSERT INTO AccountChannel VALUES('" + account.identity + "','"
+ channel.identity + "','"
+ alias + "','"
+ ordinal.get().toString() + "')";
try { connection.createStatement().executeUpdate(msql); }
catch (SQLException e) { exception.accept(e) ; }
ordinal.accept(ordinal.get() + 1);
});
Util.throwMaybe(exception.getMaybe());
} else {
throw new UpdatedException(current);
}
return updated;
}
@Override
public synchronized void delete(Stored<Account> account)
throws UpdatedException,
DeletedException,
SQLException {
final Stored<Account> current = get(account.identity);
if(current.version.equals(account.version)) {
String sql = "DELETE FROM Account WHERE id ='" + account.identity + "'";
connection.createStatement().executeUpdate(sql);
} else {
throw new UpdatedException(current);
}
}
@Override
public Stored<Account> get(UUID id)
throws DeletedException,
SQLException {
final String accountsql = "SELECT version,user,password FROM Account WHERE id = '" + id.toString() + "'";
final String channelsql = "SELECT channel,alias,ordinal FROM AccountChannel WHERE account = '" + id.toString() + "' ORDER BY ordinal DESC";
final Statement accountStatement = connection.createStatement();
final Statement channelStatement = connection.createStatement();
final ResultSet accountResult = accountStatement.executeQuery(accountsql);
final ResultSet channelResult = channelStatement.executeQuery(channelsql);
if(accountResult.next()) {
final UUID version = UUID.fromString(accountResult.getString("version"));
final UUID userid =
UUID.fromString(accountResult.getString("user"));
final String password =
accountResult.getString("password");
final Stored<User> user = userStore.get(userid);
// Get all the channels associated with this account
final List.Builder<Pair<String,Stored<Channel>>> channels = List.builder();
while(channelResult.next()) {
final UUID channelId =
UUID.fromString(channelResult.getString("channel"));
final String alias = channelResult.getString("alias");
channels.accept(
new Pair<String,Stored<Channel>>(
alias,channelStore.get(channelId)));
}
return (new Stored<Account>(new Account(user,channels.getList(),password),id,version));
} else {
throw new DeletedException();
}
}
/**
* Look up an account based on their username.
*/
public Stored<Account> lookup(String username)
throws DeletedException,
SQLException {
final String sql = "SELECT Account.id from Account INNER JOIN User ON user=User.id where User.name='" + username + "'";
System.err.println(sql);
final Statement statement = connection.createStatement();
final ResultSet rs = statement.executeQuery(sql);
if(rs.next()) {
final UUID identity =
UUID.fromString(rs.getString("id"));
return get(identity);
}
throw new DeletedException();
}
}
package inf226.inchat;
import inf226.util.immutable.List;
import inf226.storage.Stored;
import java.time.Instant;
import java.util.UUID;
/**
* The Channel class represents a channel.
*/
public final class Channel {
public final String name;
public final List<Stored<Event>> events;
/**
* Construct a Channel object from name and events.
*/
public Channel(String name, List<Stored<Event>> events) {
this.name=name;
this.events=events;
}
/**
* Post a new event to the channel.
*/
public Channel postEvent(Stored<Event> event) {
return new Channel(name, List.cons(event,events));
}
/**
* The Event class represents different kinds of events
* in a channel, such as "join events" and "message events".
*/
public static class Event {
public static enum Type {
message(0),join(1);
public final Integer code;
Type(Integer code){this.code=code;}
public static Type fromInteger(Integer i) {
if (i.equals(0))
return message;
else if (i.equals(1))
return join;
else
throw new IllegalArgumentException("Invalid Channel.Event.Type code:" + i);
}
};
public final UUID channel;
public final Type type;
public final Instant time;
public final String sender;
public final String message;
/**
* Copy constructor
*/
public Event(UUID channel, Instant time, String sender, Type type, String message) {
if (time == null) {
throw new IllegalArgumentException("Event time cannot be null");
}
if (type.equals(message) && message == null) {
throw new IllegalArgumentException("null in Event creation");
}
this.channel=channel;
this.time =time;
this.sender =sender;
this.type =type;