1
0
mirror of https://git.sr.ht/~cadence/bibliogram synced 2024-11-22 16:17:29 +00:00

CSRF and various enhancements

This commit is contained in:
Cadence Ember 2020-05-10 03:20:13 +12:00
parent 270a662c75
commit 47cc40bc5a
No known key found for this signature in database
GPG Key ID: 128B99B1B74A6412
10 changed files with 95 additions and 26 deletions

4
package-lock.json generated
View File

@ -2738,8 +2738,8 @@
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
}, },
"pinski": { "pinski": {
"version": "github:cloudrac3r/pinski#61809e18606265ec47813f3bdab80928c70ae203", "version": "github:cloudrac3r/pinski#059bfb3c07f36b7e175bce52e4715646b6acfebd",
"from": "github:cloudrac3r/pinski#61809e18606265ec47813f3bdab80928c70ae203", "from": "github:cloudrac3r/pinski#059bfb3c07f36b7e175bce52e4715646b6acfebd",
"requires": { "requires": {
"mime": "^2.4.4", "mime": "^2.4.4",
"pug": "^2.0.3", "pug": "^2.0.3",

View File

@ -19,7 +19,7 @@
"mixin-deep": "^2.0.1", "mixin-deep": "^2.0.1",
"node-dir": "^0.1.17", "node-dir": "^0.1.17",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"pinski": "github:cloudrac3r/pinski#61809e18606265ec47813f3bdab80928c70ae203", "pinski": "github:cloudrac3r/pinski#059bfb3c07f36b7e175bce52e4715646b6acfebd",
"pug": "^2.0.4", "pug": "^2.0.4",
"semver": "^7.2.1", "semver": "^7.2.1",
"sharp": "^0.25.2", "sharp": "^0.25.2",

View File

@ -166,6 +166,7 @@ let constants = {
instance_list_cache_time: 3*60*1000, instance_list_cache_time: 3*60*1000,
updater_cache_time: 2*60*1000, updater_cache_time: 2*60*1000,
cache_sweep_interval: 3*60*1000, cache_sweep_interval: 3*60*1000,
csrf_time: 60*60*1000,
self_blocked_status: { self_blocked_status: {
enabled: true, enabled: true,
time: 2*60*60*1000, time: 2*60*60*1000,
@ -232,7 +233,7 @@ let constants = {
additional_routes: [], additional_routes: [],
database_version: 6 database_version: 7
} }
// Override values from config and export the result // Override values from config and export the result

View File

@ -85,6 +85,15 @@ const deltas = new Map([
db.prepare("ALTER TABLE UserSettings ADD COLUMN rewrite_twitter TEXT NOT NULL DEFAULT ''") db.prepare("ALTER TABLE UserSettings ADD COLUMN rewrite_twitter TEXT NOT NULL DEFAULT ''")
.run() .run()
})() })()
}],
// version 6 to version 7
[7, function() {
db.transaction(() => {
db.prepare("DROP TABLE IF EXISTS CSRFTokens")
.run()
db.prepare("CREATE TABLE CSRFTokens (token TEXT NOT NULL, expires INTEGER NOT NULL, PRIMARY KEY (token))")
.run()
})()
}] }]
]) ])

View File

@ -1,6 +1,6 @@
const constants = require("../../lib/constants") const constants = require("../../lib/constants")
const {render, redirect} = require("pinski/plugins") const {render, redirect} = require("pinski/plugins")
const {getSettings} = require("./utils/getsettings") const {getSettings, getToken, generateCSRF, checkCSRF} = require("./utils/getsettings")
const crypto = require("crypto") const crypto = require("crypto")
const db = require("../../lib/db") const db = require("../../lib/db")
@ -9,13 +9,22 @@ module.exports = [
route: "/settings", methods: ["GET"], code: async ({req, url}) => { route: "/settings", methods: ["GET"], code: async ({req, url}) => {
const settings = getSettings(req) const settings = getSettings(req)
// console.log(settings) // console.log(settings)
const saved = url.searchParams.has("saved") const csrf = generateCSRF()
return render(200, "pug/settings.pug", {saved, constants, settings}) const message = url.searchParams.get("message")
const status = url.searchParams.get("status")
return render(200, "pug/settings.pug", {constants, settings, csrf, status, message})
} }
}, },
{ {
route: "/settings", methods: ["POST"], upload: true, code: async ({body}) => { route: "/settings", methods: ["POST"], upload: true, code: async ({req, body}) => {
const oldToken = getToken(req)
const params = new URLSearchParams(body.toString()) const params = new URLSearchParams(body.toString())
if (!checkCSRF(params.get("csrf"))) {
const returnParams = new URLSearchParams()
returnParams.append("status", "fail")
returnParams.append("message", "Form timed out or reused.\n(Invalid or missing CSRF token.)")
return redirect("/settings?" + returnParams.toString(), 303)
}
const prepared = {} const prepared = {}
for (const setting of constants.user_settings) { for (const setting of constants.user_settings) {
let valueOrDefault let valueOrDefault
@ -42,14 +51,15 @@ module.exports = [
prepared.created = Date.now() prepared.created = Date.now()
const fields = constants.user_settings.map(s => s.name) const fields = constants.user_settings.map(s => s.name)
db.prepare(`INSERT INTO UserSettings (token, created, ${fields.join(", ")}) VALUES (@token, @created, ${fields.map(f => "@"+f).join(", ")})`).run(prepared) db.prepare(`INSERT INTO UserSettings (token, created, ${fields.join(", ")}) VALUES (@token, @created, ${fields.map(f => "@"+f).join(", ")})`).run(prepared)
db.prepare("DELETE FROM UserSettings WHERE token = ?").run(oldToken)
const expires = new Date(Date.now() + 4000*24*60*60*1000).toUTCString() const expires = new Date(Date.now() + 4000*24*60*60*1000).toUTCString()
return { return {
statusCode: 303, statusCode: 303,
headers: { headers: {
"Location": "/settings?saved=1", "Location": "/settings?status=success&message=Saved.",
"Set-Cookie": `settings=${prepared.token}; Path=/; Expires=${expires}; SameSite=Strict` "Set-Cookie": `settings=${prepared.token}; Path=/; Expires=${expires}; SameSite=Lax`
}, },
contentType: "text/html", contentType: "text/html; charset=UTF-8",
content: "Redirecting..." content: "Redirecting..."
} }
} }

View File

@ -1,3 +1,4 @@
const crypto = require("crypto")
const {parse} = require("cookie") const {parse} = require("cookie")
const constants = require("../../../lib/constants") const constants = require("../../../lib/constants")
@ -19,14 +20,49 @@ function addDefaults(input = {}) {
return result return result
} }
function getSettings(req) { function getToken(req) {
if (!req.headers.cookie) return addDefaults() if (!req.headers.cookie) return null
const cookie = parse(req.headers.cookie) const cookie = parse(req.headers.cookie)
const settings = cookie.settings const token = cookie.settings
if (!settings) return addDefaults() if (token) return token
const row = db.prepare("SELECT * FROM UserSettings WHERE token = ?").get(settings) else return null
if (!row) return addDefaults()
return addDefaults(row)
} }
function getSettings(req) {
const token = getToken(req)
if (token) {
const row = db.prepare("SELECT * FROM UserSettings WHERE token = ?").get(token)
if (row) {
return addDefaults(row)
}
}
return addDefaults()
}
function generateCSRF() {
const token = crypto.randomBytes(16).toString("hex")
const expires = Date.now() + constants.caching.csrf_time
db.prepare("INSERT INTO CSRFTokens (token, expires) VALUES (?, ?)").run(token, expires)
return token
}
function checkCSRF(token) {
const row = db.prepare("SELECT * FROM CSRFTokens WHERE token = ? AND expires > ?").get(token, Date.now())
if (row) {
db.prepare("DELETE FROM CSRFTokens WHERE token = ?").run(token)
return true
} else {
return false
}
}
function cleanCSRF() {
db.prepare("DELETE FROM CSRFTokens WHERE expires <= ?").run(Date.now())
}
cleanCSRF()
setInterval(cleanCSRF, constants.caching.csrf_time)
module.exports.getToken = getToken
module.exports.getSettings = getSettings module.exports.getSettings = getSettings
module.exports.generateCSRF = generateCSRF
module.exports.checkCSRF = checkCSRF

View File

@ -9,6 +9,7 @@ html
header header
h1.banner h1.banner
img.banner-image(src="/static/img/banner-min.svg" alt="Bibliogram") img.banner-image(src="/static/img/banner-min.svg" alt="Bibliogram")
.go-sections-container .go-sections-container
.go-sections .go-sections
section section
@ -21,6 +22,7 @@ html
form(method="get" action="/p").pair-entry form(method="get" action="/p").pair-entry
input(type="text" name="p" placeholder="Shortcode or URL").text input(type="text" name="p" placeholder="Shortcode or URL").text
input(type="submit" value="Go").button input(type="submit" value="Go").button
.about-container .about-container
section.about section.about
h2 About Bibliogram h2 About Bibliogram
@ -31,23 +33,25 @@ html
p. p.
Bibliogram does #[em not] allow you to anonymously post, like, comment, follow, or view private profiles. Bibliogram does #[em not] allow you to anonymously post, like, comment, follow, or view private profiles.
It does not preserve deleted posts. It does not preserve deleted posts.
h2 About this instance h2 About this instance
ul ul
li: a(href="/settings") Settings
if hasPrivacyPolicy if hasPrivacyPolicy
li: a(href="/privacy") Privacy policy li: a(href="/privacy") Privacy policy
else else
li Owner has not written a privacy policy li Owner has not written a privacy policy
li Instance is #{allUnblocked ? "not blocked" : "blocked"} li Instance is #{allUnblocked ? "not blocked" : "blocked"}
li RSS feeds are #{rssEnabled ? "enabled" : "disabled"} li RSS feeds are #{rssEnabled ? "enabled" : "disabled"}
li Tor is #{torAvailable ? "enabled" : "not available"}
h2 External links h2 External links
ul.link-list ul
- -
const links = [ const links = [
["https://github.com/cloudrac3r/bibliogram", "GitHub repository"], ["https://github.com/cloudrac3r/bibliogram", "GitHub repository"],
["https://riot.im/app/#/room/#bibliogram:matrix.org", "Discussion room on Matrix"], ["https://riot.im/app/#/room/#bibliogram:matrix.org", "Discussion room on Matrix"],
["https://github.com/cloudrac3r/bibliogram/wiki/Instances", "Other Bibliogram instances"], ["https://github.com/cloudrac3r/bibliogram/wiki/Instances", "Other Bibliogram instances"],
["https://github.com/cloudrac3r/bibliogram/projects/1?fullscreen=true", "Project board"], ["https://github.com/cloudrac3r/bibliogram/projects/1?fullscreen=true", "Project roadmap"],
["https://cadence.moe/about/contact", "Contact the developer"] ["https://cadence.moe/about/contact", "Contact the developer"]
] ]
each entry in links each entry in links

View File

@ -1,4 +1,4 @@
//- Needs saved, settings //- Needs constants, settings, csrf, status, message
mixin fieldset(name) mixin fieldset(name)
fieldset fieldset
@ -31,12 +31,14 @@ html
title Settings | Bibliogram title Settings | Bibliogram
include includes/head include includes/head
body.settings-page body.settings-page
if saved if status && message
.status-notice Saved. .status-notice(class=status)= message
script. script.
history.replaceState(null, "", "/settings") history.replaceState(null, "", "/settings")
main.settings main.settings
form(action="/settings" method="post" enctype="application/x-www-form-urlencoded") form(action="/settings" method="post" enctype="application/x-www-form-urlencoded")
input(type="hidden" name="csrf" value=csrf)
h1 Settings h1 Settings
+fieldset("Features") +fieldset("Features")

View File

@ -668,10 +668,16 @@ body
.status-notice .status-notice
padding: 15px padding: 15px
font-size: 24px font-size: 24px
line-height: 1 line-height: 1.36
text-align: center text-align: center
background-color: map-get($theme, "background-banner-success")
color: map-get($theme, "foreground-banner") color: map-get($theme, "foreground-banner")
white-space: pre-line
&.success
background-color: map-get($theme, "background-banner-success")
&.fail
background-color: map-get($theme, "background-banner-fail")
.action-container .action-container
margin-top: 20px margin-top: 20px

View File

@ -22,6 +22,7 @@ $theme: (
"background-primary-quote": #ccc, "background-primary-quote": #ccc,
"background-error-page": #191919, "background-error-page": #191919,
"background-alert": #282828, "background-alert": #282828,
"background-banner-fail": #5f1111,
"background-banner-success": #0b420b, "background-banner-success": #0b420b,
"foreground-primary": #111, "foreground-primary": #111,
"foreground-header": #000, "foreground-header": #000,