Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ RUN apt-get install -y apache2
RUN pip install -U pip
RUN pip install -U flask
RUN pip install -U flask-cors
RUN pip install -U pytz
RUN pip install -U passlib
RUN echo "ServerName localhost " >> /etc/apache2/apache2.conf
RUN echo "$user hard nproc 20" >> /etc/security/limits.conf
ADD ./src/service /service
Expand Down
46 changes: 27 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,48 +96,56 @@ Please fill out this section with details relevant to your team.

### Team Members

1. Member 1 Name
2. Member 2 Name
3. Member 3 Name
4. Member 4 Name
1. Eldric Lim
2. Joshua Che
3. Chan Jian Hui
4. Derek Kok

### Short Answer Questions

#### Question 1: Briefly describe the web technology stack used in your implementation.

Answer: Please replace this sentence with your answer.
Answer: Python Flask for the web application, sqlite3 for the backend-database. We chose sqlite3 as it is light-weight and easy to use with Python Flask.

#### Question 2: Are there any security considerations your team thought about?

Answer: Please replace this sentence with your answer.
Answer: Yes, we initially made a user's token "" (empty string) when expired. This allowed requests to be authenticated by simply passing an empty string in a http request. We proceeded to change it to NULL instead.

#### Question 3: Are there any improvements you would make to the API specification to improve the security of the web application?

Answer: Please replace this sentence with your answer.
Answer: Make it https-compliant, have more parameters like nonces or timeouts for things like requests or tokens, etc.

#### Question 4: Are there any additional features you would like to highlight?

Answer: Please replace this sentence with your answer.
Answer: Our front-end code is incomplete, features such as deletion and changing of permission on diary entries are not implemented.

#### Question 5: Is your web application vulnerable? If yes, how and why? If not, what measures did you take to secure it?

Answer: Please replace this sentence with your answer.
Answer:
Yes. To facilitate user access via tokens, a user's token is stored within the webpage as a hidden field. An attacker may be able to output the value of the field by launching an XSS attack, outputting it via an alert message.

No steps were taken to prevent SQL injection vulnerabilities either.

As stated in question 2, our application initially allowed blank tokens, which could authenticate a request when a blank token is used, but we fixed this by using NULL instead.

#### Feedback: Is there any other feedback you would like to give?

Answer: Please replace this sentence with your answer.
Answer: None.

### Declaration

#### Please declare your individual contributions to the assignment:

1. Member 1 Name
- Integrated feature x into component y
- Implemented z
2. Member 2 Name
- Wrote the front-end code
3. Member 3 Name
- Designed the database schema
4. Member 4 Name
- Implemented x
1. Eldric Lim
- API
- Front-end
2. Joshua Che
- API
- Front-end
3. Chan Jian Hui
- API
- Front-end
4. Derek Kok
- API
- Front-end

77 changes: 77 additions & 0 deletions Sample cURL requests
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
==VERY IMPORTANT POINTS==

Each GET/POST request is sent with correct parameters as specified in the assignment API. Wrong request parameters may cause KeyErrors or other errors.

Queries/operations done on non-existent records --but with a valid token-- will return HTTP 200 OK. (e.g. /diary/delete with valid token but non-existent diary id)

Database file is in src/service/database.db, and it contains 2 tables "users" and "diaries".
Use DB Browser for SQLite to see how the tables look like, I've included 3 users by default in the "users" table.
The "diaries" table is empty, and requires us to populate it by using sample queries that I've included.

Since this web server is run from Docker, I've taken the liberty to make the database volatile; you can only use DB Browser to look and edit the table layouts, but won't be able to see real-time data inside the tables. To do that, observe the output when you submit curl requests, or enter the URL in firefox to see if the output is correct.



==INSTRUCTIONS==

Run these POST curl requests in a terminal, or submit your own http GET/POST request with any tool. GET requests can be done by simply opening the respective page in Firefox. (e.g. http://localhost:8080/meta/members)



==REGISTER A USER==

curl -i -X POST -H 'Content-Type: application/json' -d '{"username": "testuser", "password": "testpass", "fullname": "test full name", "age": 2009}' http://127.0.0.1:8080/users/register



==AUTHENTICATE A USER==

curl -i -X POST -H 'Content-Type: application/json' -d '{"username": "testuser", "password": "testpass"}' http://127.0.0.1:8080/users/authenticate



==RETURNS WHICHEVER USER REPRESENTATED BY THIS TOKEN==

curl -i -X POST -H 'Content-Type: application/json' -d '{"token":"111111111111111111111111111111111111"}' http://127.0.0.1:8080/users



==CREATES A --PUBLIC-- DIARY FOR AN AUTHENTICATED USER==

curl -i -X POST -H 'Content-Type: application/json' -d '{"token":"111111111111111111111111111111111111", "title": "No One Can See This Post", "public": true, "text": "It is very secret!"}' http://127.0.0.1:8080/diary/create



==CREATES A --PRIVATE-- DIARY FOR AN AUTHENTICATED USER==

curl -i -X POST -H 'Content-Type: application/json' -d '{"token":"111111111111111111111111111111111111", "title": "No One Can See This Post", "public": true, "text": "It is very secret!"}' http://127.0.0.1:8080/diary/create



==LISTS ALL DIARY ENTRIES THAT AN AUTHENTICATED USER HAS==

curl -i -X POST -H 'Content-Type: application/json' -d '{"token":"111111111111111111111111111111111111"}' http://127.0.0.1:8080/diary



==SETS AN AUTHENTICATED USER'S DIARY ENTRY TO PRIVATE==

curl -i -X POST -H 'Content-Type: application/json' -d '{"token":"111111111111111111111111111111111111", "id":1, "public":false}' http://127.0.0.1:8080/diary/permission



==SETS AN AUTHENTICATED USER'S DIARY ENTRY TO PUBLIC==

curl -i -X POST -H 'Content-Type: application/json' -d '{"token":"111111111111111111111111111111111111", "id":1, "public":false}' http://127.0.0.1:8080/diary/permission



==DELETE AN AUTHENTICATED USER'S DIARY ENTRY==

curl -i -X POST -H 'Content-Type: application/json' -d '{"token":"111111111111111111111111111111111111", "id":1}' http://127.0.0.1:8080/diary/delete



==EXPIRES AN AUTHENTICATED USER'S TOKEN==

curl -i -X POST -H 'Content-Type: application/json' -d '{"token":"111111111111111111111111111111111111"}' http://127.0.0.1:8080/users/expire
Binary file modified img/samplescreenshot.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
197 changes: 197 additions & 0 deletions src/html/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@ function ajax_get(url, callback) {
xmlhttp.send();
}

function ajax_post(url, jsondata, callback) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
console.log('responseText:' + xmlhttp.responseText);
try {
var data = JSON.parse(xmlhttp.responseText);
} catch(err) {
console.log(err.message + " in " + xmlhttp.responseText);
return;
}
callback(data);
}
};

xmlhttp.open("POST", url, true);
xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xmlhttp.send(jsondata);
}

ajax_get(API_ENDPOINT + '/meta/heartbeat', function(data) {
if (data.status) {
document.getElementById("demo_heartbeat").innerHTML = "Heartbeat success";
Expand All @@ -45,3 +65,180 @@ ajax_get(API_ENDPOINT + '/meta/members', function(data) {
}
});

ajax_get(API_ENDPOINT + '/diary', function(data) {
if (data.status) {
var diaries = data.result;
var output = "<ul>";
for (var i = 0; i < diaries.length; i++) {
entry = diaries[i];
temp_date = entry.publish_date;
publish_date = temp_date.substring(0, 10)
publish_time = temp_date.substring(11, 16)
output += "<li>";
output += "<strong>" + entry.title + "</strong>";
output += "<br>";
output += "<small>Written by " + entry.author + " on " + publish_date + " at " + publish_time + "</small>";
output += "<hr>" + entry.text + "</li>";
}
output += "</ul><br>";
document.getElementById("demo_diary").innerHTML = output;
}
else {
document.getElementById("demo_diary").innerHTML = "Diary output failed";
}
});

const isValidElement = element => {
return element.name && element.value;
};

const formToJSON = elements => [].reduce.call(elements, (data, element) => {

if (isValidElement(element)){
if (element.name == "public"){
if (element.checked){
data[element.name] = true;
}
else {
data[element.name] = false;
}
}
else {
data[element.name] = element.value;
}
}
return data;

}, {});

const handleLoginFormSubmit = event => {
url = API_ENDPOINT + "/users/authenticate";
event.preventDefault();
const data = formToJSON(loginform.elements);
var loggeduser = data["username"];
jsondata = JSON.stringify(data, null, " ");

ajax_post(url, jsondata, function(data) {
var result = data.result
if (data.status) {
var output = "<input type=\"submit\" value=\"Logout\" onclick=\"userLogOut()\"/>";
document.getElementById("login-feedback").innerHTML = "Logged in as " + loggeduser + ". " + output;
document.getElementById("token").value = data.result.token;
document.getElementById("create-token").value = data.result.token;
}
else {
document.getElementById("login-feedback").innerHTML = "Login failed";
document.getElementById("token").value = "";
document.getElementById("create-token").value = "";
}
});

loginform.reset();
};

const handleRegisterFormSubmit = event => {
url = API_ENDPOINT + "/users/register";
event.preventDefault();
const data = formToJSON(registerform.elements);
jsondata = JSON.stringify(data, null, " ");

ajax_post(url, jsondata, function(data) {
if (data.status) {
document.getElementById("register-feedback").innerHTML = "Successfully registered! Please login to continue.";
}
else {
document.getElementById("register-feedback").innerHTML = "Registration failed.";
}
});

registerform.reset()
};

const retrieveDiarySubmit = event => {
url = API_ENDPOINT + '/diary'
event.preventDefault();
const data = formToJSON(retrievediary.elements);
jsondata = JSON.stringify(data, null, " ");
document.getElementById("my-diaries").innerHTML = "Please log in to view your diary.";

ajax_post(API_ENDPOINT + '/diary', jsondata, function(data) {
var diaries = data.result;
var output = "<ul>";
for (var i = 0; i < diaries.length; i++) {
entry = diaries[i];
temp_date = entry.publish_date;
publish_date = temp_date.substring(0, 10)
publish_time = temp_date.substring(11, 16)
output += "<li>";
output += "<strong>" + entry.title + "</strong>";
output += "<br>";
output += "<small>Written on " + publish_date + " at " + publish_time + "</small>";
output += "<hr>" + entry.text + "<hr>";
output += "<span><form method=\"post\" id=\"delete-diary\">";
output += "<input type=\"hidden\" value=\"" + entry.id + "\" name=\"id\">";
output += "<input type=\"submit\" value=\"Delete\" class=\"btn btn-danger\" style=\"float: right\"></form></span>";
output += "</li>";
}
output += "</ul>";
if (data.status) {
document.getElementById("my-diaries").innerHTML = output;
}
else {
document.getElementById("my-diaries").innerHTML = "Please log in to view your diaries.";
}
});
};

const deleteDiarySubmit = event => {
url = API_ENDPOINT + "/diary/delete";
event.preventDefault();
const data = formToJSON(deletediary.elements);
jsondata = JSON.stringify(data, null, " ");

ajax_post(url, jsondata, function(data) {
if (data.status) {
document.getElementById("diary-feedback").innerHTML = "Entry deleted";
}
else {
document.getElementById("diary-feedback").innerHTML = "Invalid authentication";
}
});
};

const loginform = document.getElementById('login_form');
const registerform = document.getElementById('register_form');
const retrievediary = document.getElementById('retrieve-diary');
const deletediary = document.getElementById('delete-diary');

loginform.addEventListener('submit', handleLoginFormSubmit);
registerform.addEventListener('submit', handleRegisterFormSubmit);
retrievediary.addEventListener('submit', retrieveDiarySubmit);
deletediary.addEventListener('submit', deleteDiarySubmit);

function userLogOut(){
location.reload();
//document.getElementById("token").value = "";

//document.getElementById("my-diaries").innerHTML = "";
//document.getElementById("login-feedback").innerHTML = "Logged out!";
}

function createDiary(){
const creatediary = document.getElementById('create_diary');

url = API_ENDPOINT + "/diary/create";
event.preventDefault();
const data = formToJSON(creatediary.elements);
jsondata = JSON.stringify(data, null, " ");

ajax_post(url, jsondata, function(data) {
if (data.status) {
document.getElementById("create-feedback").innerHTML = "Entry created";
}
else {
document.getElementById("create-feedback").innerHTML = "Invalid authentication";
}
});

creatediary.reset();
}
Loading