Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Csrf tokens #65

Merged
merged 23 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 22 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
29 changes: 23 additions & 6 deletions assets/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@ async function saveConfig() {
const pkeyInput = document.getElementById("private-key").value;
const formAlert = document.getElementById("form-alert");
const formButton = document.getElementById('config-button');
const molly_csrf = document.getElementById("molly-csrf").value.trim();
formButton.classList.add("disabled");
formButton.innerHTML = `<i class="fa-solid fa-spinner animate-spin"></i>`
formButton.innerHTML = `Processing <i class="fa-solid fa-spinner animate-spin text-primary-800"></i>`
formButton.disabled = true;
if (ipInput === '' || portInput === '' || certificateInput === '' || pkeyInput === '') {
formAlert.classList.remove("hidden");
formAlert.classList.add("text-secondary-500");
Expand All @@ -96,7 +98,13 @@ async function saveConfig() {
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ "server_ip": ipInput, "server_port": Number(portInput), "certificate": certificateInput, "private_key": pkeyInput })
body: JSON.stringify({
"server_ip": ipInput,
"server_port": Number(portInput),
"certificate": certificateInput,
"private_key": pkeyInput,
"molly_csrf": molly_csrf
})
})
const data = await response.json();
if (data.status === 200) {
Expand All @@ -119,6 +127,7 @@ async function saveConfig() {
}
}
formButton.innerHTML = "Update"
formButton.disabled = false;
}

function closeBanner() {
Expand Down Expand Up @@ -146,6 +155,7 @@ async function deployUnikernel() {
const name = document.getElementById("unikernel-name").value.trim();
const arguments = document.getElementById("unikernel-arguments").value.trim();
const binary = document.getElementById("unikernel-binary").files[0];
const molly_csrf = document.getElementById("molly-csrf").value.trim();
const formAlert = document.getElementById("form-alert");
if (!name || !binary) {
formAlert.classList.remove("hidden", "text-primary-500");
Expand All @@ -158,6 +168,7 @@ async function deployUnikernel() {
formData.append("name", name);
formData.append("binary", binary)
formData.append("arguments", arguments)
formData.append("molly_csrf", molly_csrf)
try {
const response = await fetch("/unikernel/create", {
method: 'POST',
Expand Down Expand Up @@ -191,10 +202,13 @@ async function deployUnikernel() {

async function destroyUnikernel(name) {
try {
const molly_csrf = document.getElementById("molly-csrf").value.trim();
const response = await fetch(`/unikernel/destroy/${name}`, {
method: 'GET',
mode: "no-cors"
method: 'POST',
body: JSON.stringify({ "name": name, "molly_csrf": molly_csrf }),
headers: { 'Content-Type': 'application/json' }
})

const data = await response.json();
if (data.status === 200) {
postAlert("bg-primary-300", `Successful: ${data.data}`);
Expand Down Expand Up @@ -225,9 +239,10 @@ function buttonLoading(btn, load, text) {

async function toggleUserStatus(uuid, endpoint) {
try {
const molly_csrf = document.getElementById("molly-csrf").value.trim();
const response = await fetch(endpoint, {
method: 'POST',
body: JSON.stringify({ uuid: uuid }),
body: JSON.stringify({ uuid, molly_csrf }),
headers: { 'Content-Type': 'application/json' }
});

Expand Down Expand Up @@ -282,6 +297,7 @@ async function updatePolicy() {
const formAlert = document.getElementById("form-alert");
const user_id = document.getElementById("user_id").innerText;
const policyButton = document.getElementById("set-policy-btn");
const molly_csrf = document.getElementById("molly-csrf").value.trim();
try {
buttonLoading(policyButton, true, "Processing...")
const response = await fetch("/api/admin/u/policy/update", {
Expand All @@ -296,7 +312,8 @@ async function updatePolicy() {
"block": Number(storage_size),
"cpuids": cpuids,
"bridges": bridges,
"user_uuid": user_id
"user_uuid": user_id,
"molly_csrf": molly_csrf
})
})
const data = await response.json();
Expand Down
84 changes: 79 additions & 5 deletions middleware.ml
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
type handler = Httpaf.Reqd.t -> unit Lwt.t
type middleware = handler -> handler

let has_session_cookie (reqd : Httpaf.Reqd.t) =
let get_csrf now =
User_model.(
generate_cookie ~name:"molly_csrf"
~uuid:(Uuidm.to_string (generate_uuid ()))
~created_at:now ~expires_in:3600)

let has_cookie cookie_name (reqd : Httpaf.Reqd.t) =
let headers = (Httpaf.Reqd.request reqd).headers in
match Httpaf.Headers.get headers "Cookie" with
| Some cookies ->
Expand All @@ -10,7 +16,7 @@ let has_session_cookie (reqd : Httpaf.Reqd.t) =
(fun cookie ->
let parts = String.trim cookie |> String.split_on_char '=' in
match parts with
| [ name; _ ] -> String.equal name "molly_session"
| [ name; _ ] -> String.equal name cookie_name
| _ -> false)
cookie_list
| _ -> None
Expand Down Expand Up @@ -92,13 +98,32 @@ let redirect_to_dashboard reqd ?(msg = "") () =
Httpaf.Reqd.respond_with_string reqd response msg;
Lwt.return_unit

let cookie_value_from_auth_cookie cookie =
let http_response ~title ?(header_list = []) ?(data = "") reqd http_status =
let code = Httpaf.Status.to_code http_status
and success = Httpaf.Status.is_successful http_status in
let status = { Utils.Status.code; title; data; success } in
let data = Utils.Status.to_json status in
let headers =
Httpaf.Headers.(
add_list
(of_list
[
("Content-Type", "application/json");
("Content-length", string_of_int (String.length data));
])
header_list)
in
let response = Httpaf.Response.create ~headers http_status in
Httpaf.Reqd.respond_with_string reqd response data;
Lwt.return_unit

let cookie_value cookie =
match String.split_on_char '=' (String.trim cookie) with
| _ :: s :: _ -> Ok (String.trim s)
hannesm marked this conversation as resolved.
Show resolved Hide resolved
| _ -> Error (`Msg "Bad cookie")

let user_from_auth_cookie cookie users =
match cookie_value_from_auth_cookie cookie with
match cookie_value cookie with
| Ok cookie_value -> (
match User_model.find_user_by_key cookie_value users with
| Some user -> Ok user
Expand All @@ -108,7 +133,7 @@ let user_from_auth_cookie cookie users =
Error (`Msg s)

let user_of_cookie users now reqd =
match has_session_cookie reqd with
match has_cookie "molly_session" reqd with
| Some auth_cookie -> (
match user_from_auth_cookie auth_cookie users with
| Ok user -> (
Expand Down Expand Up @@ -137,6 +162,15 @@ let user_of_cookie users now reqd =
m "auth-middleware: No molly-session in cookie header.");
Error (`Msg "User not found")

let session_cookie_value reqd =
match has_cookie "molly_session" reqd with
| Some cookie -> (
match cookie_value cookie with
| Ok "" -> Ok None
| Ok x -> Ok (Some x)
| Error _ as e -> e)
| None -> Error (`Msg "no cookie found")

let auth_middleware now users handler reqd =
match user_of_cookie users now reqd with
| Ok user ->
Expand All @@ -161,3 +195,43 @@ let is_user_admin_middleware api_meth now users handler reqd =
"You don't have the necessary permissions to access this service."
`Unauthorized user 401 api_meth reqd ()
| Error (`Msg msg) -> redirect_to_login ~msg reqd ()

let csrf_match ~input_csrf ~check_csrf =
String.equal (Utils.Json.clean_string input_csrf) check_csrf
hannesm marked this conversation as resolved.
Show resolved Hide resolved

let csrf_cookie_verification form_csrf reqd =
match has_cookie "molly_csrf" reqd with
| Some cookie -> (
match cookie_value cookie with
| Ok token -> csrf_match ~input_csrf:form_csrf ~check_csrf:token
| Error (`Msg err) ->
Logs.err (fun m -> m "Error retrieving csrf value from cookie %s" err);
false)
| None ->
Logs.err (fun m -> m "Couldn't find csrf cookie.");
false

let csrf_verification users now form_csrf handler reqd =
match user_of_cookie users now reqd with
| Ok user -> (
let user_csrf_token =
List.find_opt
(fun (cookie : User_model.cookie) ->
String.equal cookie.name "molly_csrf")
user.User_model.cookies
in
match user_csrf_token with
| Some csrf_token ->
if
User_model.is_valid_cookie csrf_token now
&& csrf_match ~check_csrf:csrf_token.value ~input_csrf:form_csrf
then handler reqd
else
http_response ~title:"CSRF Token Mismatch"
~data:"CSRF token mismatch error. Please referesh and try again."
reqd `Bad_request
| None ->
http_response
~data:"Missing CSRF token. Please referesh and try again."
~title:"Missing CSRF Token" reqd `Bad_request)
| Error (`Msg err) -> redirect_to_login ~msg:err reqd ()
3 changes: 2 additions & 1 deletion settings_page.ml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
let settings_layout (configuration : Configuration.t) =
let settings_layout ~csrf (configuration : Configuration.t) =
let ip = Ipaddr.to_string configuration.server_ip in
let port = string_of_int configuration.server_port in
let certificate = X509.Certificate.encode_pem configuration.certificate in
Expand All @@ -11,6 +11,7 @@ let settings_layout (configuration : Configuration.t) =
div
~a:[ a_class [ "px-3 flex justify-between items-center" ] ]
[
Utils.csrf_form_input csrf;
div
[
p
Expand Down
8 changes: 5 additions & 3 deletions sign_up.ml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
open Tyxml

let register_page ~icon () =
let register_page ~csrf ~icon =
let page =
Html.(
html
Expand Down Expand Up @@ -38,6 +38,7 @@ let register_page ~icon () =
];
]
[
Utils.csrf_form_input csrf;
div
~a:[ a_class [ "w-full max-w-lg mt-16 pb-16 mx-auto" ] ]
[
Expand Down Expand Up @@ -255,7 +256,8 @@ let register_page ~icon () =
document.getElementById('register-button')\n\
\ registerButton.addEventListener('click', async \
function() {\n\
\ const name = \
const form_csrf = document.getElementById('molly-csrf').value\n\
\ const name = \
document.getElementById('name').value\n\
\ const email = \
document.getElementById('email').value\n\
Expand Down Expand Up @@ -304,7 +306,7 @@ let register_page ~icon () =
'application/json',\n\
\ },\n\
\ body: JSON.stringify({ name, \
email, password })\n\
email, password, form_csrf })\n\
\ })\n\
\ const data = await response.json();\n\
\ if (data.status === 200) {\n\
Expand Down
Loading
Loading