Sunday, August 23, 2026

(WITH-AI ...)

I vibe coded my web site, not bothering to examine the code generated by the LLM, but giving it specifically directed prompts to generate a `secure` web service. I cracked open the code today to see how it did. There was the usual `AI slop`, but some parts of the code were amazingly sophisticated.

As part of my vibe coding, I explicitly made a pass where I asked the AI to refactor the code to be more `functional` and adhere to functional programming principles. This turned out to produce some nice results. The AI refactored elements of the middleware to use some WITH-... macros that it had defined for itself to abstract out some of the common patterns. Let me show you some of what it was doing.

Cross-site request forgery (CSRF) is a common web security vulnerability. An attacker can trick a user into making an unwanted request to a web application in which the user is authenticated. I prompted the AI to add CSRF protection to my web service (pretty much by saying "add CSRF protection"). The AI generated a file specifically for CSRF protection. The file starts with this comment:

;; --- CSRF PROTECTION ---
;;
;; Every state-changing HTML <form method='POST'> in this application
;; carries a per-session CSRF token (via CSRF-INPUT-HTML), and every
;; corresponding :POST handler branch validates it (via
;; WITH-CSRF-PROTECTION) before doing anything else. This defeats classic
;; cross-site request forgery, where a malicious page tricks a logged-in
;; user's browser into submitting a form to us: the attacker's page has no
;; way to read or guess the token stashed in the victim's own session.
;;
;; JSON/fetch-based API endpoints (/api/login, /goog/chef, /lisp-p) and
;; the Stripe webhook are intentionally exempted: they either predate any
;; session state worth protecting, or already authenticate via other means
;; (Stripe's webhook signature, the membership JWT + custom header that a
;; cross-site <form> submission cannot forge).

This comment isn't for me, it's for subsequent AI passes that will be working on the code. It explains the purpose of the CSRF protection and how it works. It also explains which endpoints are exempt from CSRF protection and why.

Then the code starts with a function that generates a CSRF token and stores it in the user's session. The token is a secure random string large enough to be unguessable.

(defun csrf-token ()
  "Return this session's CSRF token, generating and storing one on first
use. Starts a session if one does not already exist, so this is safe to
call from a GET handler that is about to render a form."
  (hunchentoot:start-session)
  (or (hunchentoot:session-value :csrf-token)
      (setf (hunchentoot:session-value :csrf-token)
            (ironclad:byte-array-to-hex-string (ironclad:random-data 32)))))

Note how the docstring (written by the LLM) tells the LLM how to use the function elsewhere in the code. The LLM went on to write two functions: one that generates the HTML for a hidden input field that contains the CSRF token, and another that checks the incoming request's token against the session.

(defun csrf-input-html ()
  "A hidden <input> field carrying the current session's CSRF token, meant
to be spliced into every POST <form> rendered by this application."
  (format nil "<input type='hidden' name='csrf-token' value='~A'>" (csrf-token)))

(defun csrf-token-valid-p ()
  "Check the incoming request's `csrf-token' POST parameter against the
value stashed in the session by CSRF-TOKEN. Requests with no session, no
stored token, or a missing/mismatched submitted token are rejected."
  (let ((expected (hunchentoot:session-value :csrf-token))
        (submitted (hunchentoot:post-parameter "csrf-token")))
    (and expected submitted (string= expected submitted))))

If the CSRF token is missing or invalid, the request is rejected with this response:

(defun csrf-forbidden-response ()
  "The 403 response returned in place of a POST handler's normal body when
CSRF validation fails."
  (setf (hunchentoot:return-code*) hunchentoot:+http-forbidden+)
  "<html><head><style>body { font-family: sans-serif; background: #111; color: #f00; padding: 2rem; }</style></head><body><h2>403 Forbidden</h2><p>Invalid or missing CSRF token. Please reload the page and try again.</p></body></html>")

Now we need to wire up these primitives into the request handling.

(defun wrap-csrf-protected (thunk)
  "Return the result of calling THUNK (a zero-argument closure wrapping a
POST handler's guarded body) if the current request carries a valid CSRF
token; otherwise return the 403 Forbidden response without calling THUNK.
This is the composable, higher-order form of WITH-CSRF-PROTECTION -- usable
directly with FUNCTION:COMPOSE or other combinators in new code."
  (if (csrf-token-valid-p)
      (funcall thunk)
      (csrf-forbidden-response)))

(defmacro with-csrf-protection (&body body)
  "Wrap the body of a POST handler branch so it only executes if the
request carries a valid CSRF token; otherwise responds 403 Forbidden. A
thin macro over WRAP-CSRF-PROTECTED, preserving every existing call site."
  `(wrap-csrf-protected (lambda () ,@body)))

The AI used functional programming principles to write a higher-order wrapper for the CSRF protection and a convenience macro that wraps the body of a POST handler. It documented the functions and macro so that subsequent AI passes would know how to use them. This is pretty sophisticated. Other parts of the code simply have to write (with-csrf-protection ...) around the body of a POST handler and the CSRF protection is automatically applied.

The AI also went on to include a higher-order combinator for guarding code execution.

;; --- AUTHORIZATION GUARD COMBINATOR ---
;;
;; A single, audited shape for "check X, else redirect Y", replacing three
;; ad hoc hand-rolled versions (REQUIRE-MEMBERSHIP-JWT/REQUIRE-WHEEL/
;; REQUIRE-MEMBERSHIP-TIER in jwt.lisp, and REQUIRE-SESSION-WHEEL in
;; admin.lisp). See FUNCTIONAL_REFACTOR.md Phase 3.

(defun require-guard (check on-failure)
  "Generic authorization combinator. CHECK is a zero-argument thunk that
returns a non-NIL success value (e.g. JWT claims, or a wheel's username) or
NIL to indicate failure. ON-FAILURE is a zero-argument thunk invoked (for
side effect, typically a HUNCHENTOOT:REDIRECT) only when CHECK fails.
Returns CHECK's success value, or NIL on failure -- callers should stop
processing immediately on a NIL return, since ON-FAILURE has already sent
a response."
  (or (funcall check)
      (progn (funcall on-failure) nil)))

Several of the pages on jrm-code-project.com are protected by a membership JWT. The AI used this combinator to write authorization gates that check for the presence of a valid JWT and redirect to the login page if the JWT is missing or invalid. There are two ways to obtain a JWT. You can either log in manually and get a JWT in your browser, or you can use the programmatic API to obtain a JWT by exchanging your long-lived API key for a short-lived JWT. The JWT encodes the user's membership tier. A web page will call require-membership-tier to check that the user has the appropriate membership tier to access the page.

(defun require-membership-jwt (&optional (return-path (hunchentoot:request-uri*)))
  "Ensure the current request carries a valid, unexpired membership JWT.
Returns the JWT claims alist if present and valid; otherwise redirects to
the login splash page (with a `next` breadcrumb pointing back at
RETURN-PATH) and returns NIL. Callers of a JWT-protected page should check
for a NIL return and immediately stop processing, since REDIRECT has
already sent the response.
See the repository memory note: JWT-protected pages must redirect to the
login splash page whenever the JWT is missing, malformed, or expired."
  (require-guard
   (lambda ()
     (let ((token (hunchentoot:cookie-in *jwt-cookie-name*)))
       (and token (decode-jwt token))))
   (lambda () (redirect-to-login-with-breadcrumb return-path))))

(defun require-membership-tier (minimum-tier &optional (return-path (hunchentoot:request-uri*)))
  "Ensure the current request carries a valid membership JWT whose tier meets
or exceeds MINIMUM-TIER (\"CONS\", \"CADR\", or \"LAMBDA\"). Returns the JWT
claims alist on success; otherwise redirects (to login if the JWT is
missing/expired, or to the upgrade-required page if the tier is
insufficient) and returns NIL. Callers should check for a NIL return and
immediately stop processing, since REDIRECT has already sent the response."
  (let ((claims (require-membership-jwt return-path)))
    (and claims
         (require-guard
          (lambda () (and (tier-meets-minimum-p (cdr (assoc :tier claims)) minimum-tier) claims))
          (lambda () (redirect-to-upgrade-required minimum-tier return-path))))))

This isn't AI slop. The AI wrote some pretty good code here. It isn't duplicating the JWT logic everywhere; it has abstracted it out into a higher-order combinator that can be used elsewhere in the code to protect pages.

AI code generation has come a long way in the past year.


No comments: