Add CSRF token support

This commit is contained in:
2025-09-03 15:36:28 +01:00
parent bdbf33098f
commit 0c2a8f3d98
7 changed files with 66 additions and 22 deletions

View File

@@ -37,6 +37,15 @@ Metrics/MethodLength:
Metrics/ClassLength:
Enabled: false
Metrics/BlockLength:
Enabled: false
Layout/FirstHashElementIndentation:
Enabled: false
Layout/FirstArrayElementIndentation:
Enabled: false
Layout/EmptyLineAfterGuardClause:
Enabled: false

View File

@@ -8,6 +8,7 @@
content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
/>
<meta name="signed_in" content="<%= @signed_in %>">
<meta name="csrf" content="<%= @csrf_token %>">
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link

View File

@@ -2,10 +2,10 @@
module Logman
def self.log(log)
file, line = caller(1, 1)[0].split(":")
File.write("log/main.log", "[#{Time.now}] {#{file}:#{line}} #{log}\n", mode: "a")
File.write("log/main.log", "[#{Time.now}] {#{file}:#{line}} #{log}\n", mode: "a") if ENV_HASH["ENV"] == "dev"
end
def self.imp(log)
File.write("log/imp.log", "[#{Time.now}] #{log}\n", mode: "a")
File.write("log/imp.log", "[#{Time.now}] #{log}\n", mode: "a") if ENV_HASH["ENV"] == "dev"
end
end

37
main.rb
View File

@@ -8,8 +8,6 @@ require "uri"
require "xxhash"
require "zlib"
load "logman.rb"
ALPHANUM = [*"0".."9", *"A".."Z", *"a".."z", "-", "_"].freeze
env_data = File.exist?(".env") ? File.read(".env") : ""
@@ -20,10 +18,9 @@ env_data.each_line do |line|
ENV_HASH[match[1]] = match[2]
end
load "logman.rb"
# Logman.log ENV_HASH.inspect
CODE_ENV = :dev
db_file = File.expand_path("infinsweeper.db")
DB = Sequel.connect("sqlite:///#{db_file}", single_threaded: false)
DB.run("PRAGMA foreign_keys = ON;")
@@ -40,6 +37,8 @@ get "/" do
@message = session.message || ""
session.message = ""
@signed_in = session.signed_in?.nil? ? false : true
@csrf_token = Array.new(32) { ALPHANUM.sample }.join
session["csrf_token"] = @csrf_token
ERB.new(File.read("index.erb")).result(binding)
end
@@ -53,23 +52,28 @@ post "/signup" do
uid = session["user"]
session.logout unless uid.nil? || $active_users[uid].nil?
data = JSON.parse(request.body.read)
unless session.csrf_auth?
status 401
return { "message" => "Unauthorized (invalid CSRF token)!" }.to_json
end
session["csrf_token"] = Array.new(32) { ALPHANUM.sample }.join
if data["email"].nil? || data["pass"].nil? || data["username"].nil?
status 400
return { "message" => "Bad request made!" }.to_json
return { "message" => "Bad request made!", "csrf_token" => session["csrf_token"] }.to_json
end
signup_status = Players.mk_player(data["username"], data["email"], data["pass"])
if signup_status[0] == 200
login_status = session.login(data["username"], data["pass"])
if login_status[0] == 200
status 200
return { "message" => login_status[1], "success" => "true" }.to_json
return { "message" => login_status[1], "success" => "true", "csrf_token" => session["csrf_token"] }.to_json
else
status login_status[0]
return { "message" => login_status[1] }.to_json
return { "message" => login_status[1], "csrf_token" => session["csrf_token"] }.to_json
end
end
status signup_status[0]
return { "message" => signup_status[1] }.to_json
return { "message" => signup_status[1], "csrf_token" => session["csrf_token"] }.to_json
end
get "/verify/:code" do
@@ -82,21 +86,30 @@ post "/login" do
data = JSON.parse(request.body.read)
session = Sessions.new request, response
uid = session["user"]
Logman.log(request.env["HTTP_X_CSRF_TOKEN"].to_s + " " + session["csrf_token"].to_s)
unless session.csrf_auth?
status 401
return { "message" => "Unauthorized (invalid CSRF token)!" }.to_json
end
session["csrf_token"] = Array.new(32) { ALPHANUM.sample }.join
if $active_users[uid] && !session.logout
status 500
return { "message" => "Internal server error when signing the existing session out!" }.to_json
return {
"message" => "Internal server error when signing the existing session out!",
"csrf_token" => session["csrf_token"],
}.to_json
end
if data["username"].nil? || data["pass"].nil?
status 400
return { "message" => "Bad request made!" }.to_json
return { "message" => "Bad request made!", "csrf_token" => session["csrf_token"] }.to_json
end
login_status = session.login(data["username"], data["pass"])
if login_status[0] == 200
status 200
return { "message" => login_status[1], "success" => "true" }.to_json
return { "message" => login_status[1], "success" => "true", "csrf_token" => session["csrf_token"] }.to_json
else
status login_status[0]
return { "message" => login_status[1] }.to_json
return { "message" => login_status[1], "csrf_token" => session["csrf_token"] }.to_json
end
end

View File

@@ -9,21 +9,24 @@ module Players
end
def self.mk_player(username, email, pass)
raise ArgumentError, "Email format is wrong!" unless email.match?(/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/)
# rubocop:disable Layout/LineLength
raise ArgumentError, "Email format is wrong!" unless
email.match?(%r[(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])])
# rubocop:enable Layout/LineLength
raise ArgumentError, "Username must be at least 4 characters long and of valid format." unless
username.match?(/\A[a-zA-Z][a-zA-Z0-9_.-]+\z/) && username.length >= 4
raise ArgumentError, "Password must be at least 8 characters and of valid format." unless
pass.match?(/\A[a-zA-Z0-9_.!?@#$%^&*()+=-]+\z/) && pass.length >= 8
digest = XXhash.xxh32(pass, ENV_HASH["SALT"])
code = CODE_ENV == :prod ? Array.new(24) { ALPHANUM.sample }.join : "!"
code = ENV_HASH["ENV"] == "prod" ? Array.new(24) { ALPHANUM.sample }.join : "!"
DB[
"insert into Players (username, digest, email, activation_code) values (?, ?, ?, ?)",
username, digest, email, code
].insert
send_email(:new, email, username, code) if CODE_ENV == :prod
send_email(:new, email, username, code) if ENV_HASH["ENV"] == "prod"
[200, "Successfully signed up!"]
rescue ArgumentError => e

View File

@@ -1,3 +1,5 @@
const csrfMeta = document.querySelector('meta[name="csrf"]');
var csrf = csrfMeta?.content;
window.onload = async () => {
const popup = document.getElementById("popup");
const loginSection = document.getElementById("login");
@@ -71,11 +73,12 @@ window.onload = async () => {
const { username, pass } = loginForm;
const res = await fetch("/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf },
body: JSON.stringify({ username: username.value, pass: pass.value }),
});
const data = await res.json();
loginInfo.innerText = data.message;
csrf = data.csrf_token;
if (data.success == "true") {
loginButton.style.display = "none";
signupButton.style.display = "none";
@@ -91,7 +94,7 @@ window.onload = async () => {
const { username, email, pass } = signupForm;
const res = await fetch("/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf },
body: JSON.stringify({
username: username.value,
email: email.value,
@@ -100,6 +103,7 @@ window.onload = async () => {
});
const data = await res.json();
signupInfo.innerText = data.message;
csrf = data.csrf_token;
if (data.success == "true") {
loginButton.style.display = "none";
signupButton.style.display = "none";

View File

@@ -40,17 +40,22 @@ class Sessions
end
end
# TODO: Use .all here
def []=(key, val)
session = @request.cookies["session"]
session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session))
session = JSON.parse(session)
session[key] = val
Logman.log "Updated: #{key} to #{val}"
compressed = Zlib::Deflate.deflate(JSON.generate(session))
encoded = Base64.encode64(compressed)
@response.set_cookie("session",
value: encoded,
path: "/",
expires: Time.now + 360 * 24 * 60 * 60)
expires: Time.now + 360 * 24 * 60 * 60,
httponly: true,
secure: ENV_HASH["ENV"] == "prod",
samesite: :strict)
uid = session["user"]
DB["UPDATE SignedInUsers SET last_used_at = CURRENT_TIMESTAMP WHERE code = ?", uid].update if uid
rescue JSON::ParserError, Zlib::Error
@@ -79,13 +84,19 @@ class Sessions
@response.set_cookie("message",
value: val,
path: "/",
expires: Time.now + 360 * 24 * 60 * 60)
expires: Time.now + 360 * 24 * 60 * 60,
secure: ENV_HASH["ENV"] == "prod",
samesite: :strict)
end
def message
@request.cookies["message"]
end
def csrf_auth?
@request.env["HTTP_X_CSRF_TOKEN"] == self["csrf_token"]
end
def all
session = @request.cookies["session"]
session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session))
@@ -107,7 +118,10 @@ class Sessions
@response.set_cookie("session",
value: encoded,
path: "/",
expires: Time.now + 360 * 24 * 60 * 60)
expires: Time.now + 360 * 24 * 60 * 60,
httponly: true,
secure: ENV_HASH["ENV"] == "prod",
samesite: :strict)
rescue JSON::ParserError, Zlib::Error
@response.delete_cookie("session")
end