RSS Feed

Lisp Project of the Day

cl-who

You can support this project by donating at:

Donate using PatreonDonate using Liberapay

Or see the list of project sponsors.

cl-whowebtemplates

Documentation😀
Docstrings😀
Tests 😀
Examples😀
RepositoryActivity🥺
CI 🥺

Today we continue to investigate poftheday's dependencies and will look at the well known cl-who library. CL-Who is a library Edmund Weitz and provides a DSL for HTML generation.

For those who are not familiar with cl-who, here is a quick example:

POFTHEDAY> (cl-who:with-html-output-to-string (s)
             (:body
              (:p "Hello world!")))
"<body><p>Hello world!</p></body>"

If you want to insert a variable, you have to use a local macro esc. There is also another macro - str, and it very easy to misuse it. That is one of the reasons why I don't like cl-who and prefer spinneret.

Let's pretend we want to output a username in the comment list on our page. The correct way to do so will be:

POFTHEDAY> (defclass user ()
             ((name :initarg :name
                    :reader get-name)))

POFTHEDAY> (let ((user (make-instance
                        'user
                        :name "Bob <script>alert('You are hacked')</script>"))
                 (comment-text "Hello from Bob!"))
             (cl-who:with-html-output-to-string (s nil :indent t)
               (:div :class "comment"
                     (:div :class "username"
                           (cl-who:esc (get-name user)))
                     (:div :class "text"
                           (cl-who:esc comment-text)))))
"
<div class='comment'>
  <div class='username'>Bob &lt;script&gt;alert(&#039;You are hacked&#039;)&lt;/script&gt;
  </div>
  <div class='text'>Hello from Bob!
  </div>
</div>"

As I said, this was a correct way, but it is very easy to misuse cl-who and make your beautiful site open for XSS attacks. You only have to use str instead of esc:

POFTHEDAY> (let ((user (make-instance
                        'user
                        :name "Bob <script>alert('You are hacked')</script>"))
                 (comment-text "Hello from Bob!"))
             (cl-who:with-html-output-to-string (s nil :indent t)
               (:div :class "comment"
                     (:div :class "username"
                           (cl-who:str (get-name user)))
                     (:div :class "text"
                           (cl-who:str comment-text)))))
"
<div class='comment'>
  <div class='username'>Bob <script>alert('You are hacked')</script>
  </div>
  <div class='text'>Hello from Bob!
  </div>
</div>"

Here script tag that was not escaped. This way, any code an evil user will enter as his name will be executed in other users browsers.

Another inconvenience of cl-who is that you have to use htm macro if want to mix HTML pieces with lisp forms. For example, if you want to output a list of items, this will not work:

POFTHEDAY> (let ((list (list 1 2 3 4 5)))
             (cl-who:with-html-output-to-string (s nil :indent t)
               (:ul
                (loop for item in list
                      do (:li (cl-who:esc
                               (format nil "Item number ~A"
                                       item)))))))
; in: LET ((LIST (LIST 1 2 3 4 5)))
;     (:LI (CL-WHO:ESC (FORMAT NIL "Item number ~A" POFTHEDAY::ITEM)))
; 
; caught STYLE-WARNING:
;   undefined function: :LI
; 
; compilation unit finished
;   Undefined function:
;     :LI
;   caught 1 STYLE-WARNING condition

You have to wrap :li form with a htm macro, like that:

POFTHEDAY> (let ((list (list 1 2 3 4 5)))
             (cl-who:with-html-output-to-string (s nil :indent t)
               (:ul
                (loop for item in list
                      do (cl-who:htm
                          (:li 
                           (cl-who:esc
                            (format nil "Item number ~A"
                                    item))))))))
"
<ul>
<li>Item number 1
</li>
<li>Item number 2
</li>
<li>Item number 3
</li>
<li>Item number 4
</li>
<li>Item number 5
</li>
</ul>"

The Common Lisp Project of the Day's blog uses cl-who only because this is a dependency of the cl-bootstrap. Personally, I prefer spinneret and probably will rewrite #poftheday site to use it.

Update from 2020-09-14

The review on Spinneret was published.


Brought to you by 40Ants under Creative Commons License