Skip to content
2024

Literary Lions Forum

A book-discussion forum in Go. Server-rendered on SQLite, with no JavaScript.

Overview

We built a book-discussion forum with accounts, posts, comments, categories and likes. The two-person project connects a Go server and SQLite data to server-rendered pages and forms, with shared responsibilities across the application.

context
Coursework
status
Archived learning project
team
2 people
role
Shared responsibilities
started
Sept 2024
setting
Server-rendered Go and SQLite coursework application, packaged with Docker.

my contribution

My contributions focused on backend structure, database integration, authentication and sessions, and forum functionality, with additional frontend integration work. My teammate contributed templates, styling and application code.

built with
  • Go
  • SQLite
  • bcrypt
  • Docker

Key decisions

Keep the request path server-rendered
Go's net/http handlers render pages directly, with SQLite storing discussion and account data. The application does not depend on client-side JavaScript.
Store password hashes and server-side sessions
Passwords use bcrypt; a session cookie resolves to an expiring database row so the server can end a session rather than relying only on a client-held token.

Demonstrated results

Discussion features connect the schema to the pages
The implementation supports posts, comments, categories, sorting and a liked-posts view, alongside book and profile pages.
Password changes and logout have server-side handling
The handlers check the previous password before re-hashing and remove the session on logout. Parameterised query examples keep values separate from SQL text.

Known limitations

HTML output lacks automatic escaping
The archived handlers use text/template for HTML, leaving stored user content capable of rendering as script. This later review finding remains unfixed.
Business logic is coupled to database access
Handlers issue queries directly, making database-dependent behaviour harder to isolate for testing than in the later layered e-commerce project.

Forum features and request flow

The application handles requests with net/http and renders templates on the server. SQLite stores the discussion and account data. The feature work connects book listings, posts and comments with category filters, sorting, likes and profile views. Forms submit to the backend rather than depending on a browser-side application.

Responsibilities were shared within the two-person team. The contribution section above identifies my backend and integration work without attributing the entire interface or application to one person.

The template package was wrong

During the later review I found a template-package choice that undermines the otherwise parameterised data path.

internal/handlers/helpers.go line 7:

import "text/template"

Go has two template packages with near-identical APIs:

Package Escaping Intended for
html/template Automatic, context-aware HTML output
text/template None Plain text: config files, code generation

They are interchangeable in use, which is exactly the problem. text/template happily parses and executes an HTML file. It just inserts every value verbatim. Swapping the import is a one-line change that removes an entire vulnerability class, and there is no compiler error, no test failure, and nothing in the output that looks wrong.

The chain, start to finish

Comment bodies go from the request straight to the database and back out into the page, with no escaping anywhere in the path.

In. The handler reads the raw form value and checks only that it is non-empty:

content := r.FormValue("content")
if content == "" {
    http.Error(w, "Comment content cannot be empty", http.StatusBadRequest)
    return
}

Stored. Directly into SQLite, parameterised. There is no SQL injection, which makes the escaping gap easier to miss, because the query looks like it was handled properly:

query := `INSERT INTO Comments (post_id, user_id, content) VALUES (?, ?, ?)`
_, err = database.GetDB().Exec(query, postID, userID, content)

Out. And rendered by the non-escaping template:

<p>{{.Content}}</p>

That is at HTML/comments.html:67, and again at :112. Post titles and usernames render the same way at HTML/index.html:60, posts.html:65 and books.html:73.

So a comment containing <script>...</script> is stored verbatim and re-emitted verbatim into the HTML of every subsequent page view. Any logged-in user who posts it has run script in every other reader's browser, as the forum's own origin. That is stored XSS, and with cookies on this app the interesting part does not stop at the script tag.

The fix is to change one import to html/template. It escapes text interpolations in HTML context by default, and handles the cases that trip people up. < becomes &lt;, attribute and URL contexts are escaped differently, and template.JS becomes an explicit, greppable opt-in instead of the default.

Authentication and session handling

The account implementation includes password hashing and server-side session state.

Passwords are bcrypt with DefaultCost, and re-hashing happens on password change:

if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(oldPassword)); err != nil {
    ...
}
newPasswordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)

Sessions are server-side. A session_id cookie resolves to a row in a sessions table with an expiry. Logging out and session expiry actually revoke, rather than relying only on a signed token the client holds. This gives the application a server-side revocation path.

The schema is idempotent. Migrations run at startup with CREATE TABLE IF NOT EXISTS, so the container starts against an empty volume without a separate migration step.

The inspected queries use parameters. Examples such as the comment and likes inserts pass values through placeholders. Fixed ORDER BY clauses select supported sort options. These examples do not establish that every injection-related risk was exhaustively audited, and they do not prevent the separate HTML-escaping gap.

No repository layer

Every handler opens database/sql and writes its own queries. comments.go, books.go, index.go, helpers.go all import it directly. There is no service layer and no repository, and profile reads live in the same file as password changes.

That is a fair way to write a 2,000-line forum. It is also the first thing I would change now. The e-commerce project written later has an explicit three-layer rule: Handler may not touch the database. The contrast makes the cost concrete. There, business rules are testable without a live connection. Here, every rule lives next to a QueryRow.

Shape of the work

The history spans September 2024 to June 2026, but that is not continuous feature development. Most application work landed in September 2024. The later maintenance commit migrated Go module paths and added Docker Compose. The confirmed two-person contributions above describe the implementation work rather than inferring effort from the length of the calendar span.

Three dependencies, all of them doing real work: google/uuid for identifiers, mattn/go-sqlite3 for the cgo driver, and golang.org/x/crypto for bcrypt.

Stack and source figures

how it fits together

server
  • net/http + ServeMux 19 HandleFunc registrations and no web framework. Templates parsed once into package-level vars.
templating
  • text/template The bug, not the feature. html/template is the context-aware escaping package. text/template escapes nothing, so comment bodies render as raw HTML.
data
  • SQLite via mattn/go-sqlite3 Six tables created by an idempotent migration runner. The driver is cgo, so the Dockerfile is a necessity rather than a convenience.
auth
  • bcrypt + server-side sessions Passwords at DefaultCost; a session_id cookie resolves to a row with an expiry, so logout and expiry actually revoke.
operations
  • Docker Single-image deployment of the app and its database file.
handlers
2,117 lines Across internal/, dominated by SQL and template plumbing.
routes
19 All on the standard library's ServeMux. No web framework.
tables
6 users, posts, comments, categories, likes, sessions.
dependencies
3 uuid, go-sqlite3, golang.org/x/crypto. All three load-bearing.
commits
136 History spans 2024-09-02 to 2026-06-10. Most feature work was in September 2024; the later update migrated module paths and added Docker Compose.
Full issue record · 3 entries

what broke, and what happened

  • 3 unresolved / 3

Every entry below says what broke and what became of it. An unresolved one is listed as unresolved rather than dropped.

  1. unresolved

    The template package was text/template, not html/template

    `text/template` performs no escaping at all. `html/template` is the context-aware, auto-escaping package; `text/template` is for non-HTML output such as config files. Using it for HTML is the whole bug.

    then: Not fixed in the archived source. `internal/handlers/helpers.go:7` imports `text/template`, so running that version retains the stored-XSS vulnerability described below. The template package needs changing to `html/template`.

  2. unresolved

    Comment bodies were stored raw and rendered unescaped

    `content := r.FormValue("content")` → `INSERT INTO Comments` → `{{.Content}}` at HTML/comments.html:67 and :112.

    then: Unresolved, and it is the same root cause as the entry above rather than a second bug. Every comment body is stored verbatim and re-emitted into the page of whoever reads it next.

  3. unresolved

    Two routes read from the database with no repository layer, unlike the e-commerce project written later

    then: Left as-is deliberately. This predates the layering rule, and the project was never refactored. Every rule still lives beside a QueryRow, so nothing here is testable without a live database.