diff options
author | Jason A. Donenfeld | 2014-01-16 11:39:17 +0100 |
---|---|---|
committer | Jason A. Donenfeld | 2014-01-16 12:13:39 +0100 |
commit | b826537cb4aa2358027ffcb1dd6a87274734e962 (patch) | |
tree | 7c749c66d868cb996828d2b65a4bede58b5ebd62 /filters | |
parent | d6e9200cc35411f3f27426b608bcfdef9348e6d3 (diff) | |
download | cgit-b826537cb4aa2358027ffcb1dd6a87274734e962.tar.gz cgit-b826537cb4aa2358027ffcb1dd6a87274734e962.tar.bz2 cgit-b826537cb4aa2358027ffcb1dd6a87274734e962.zip |
authentication: use hidden form instead of referer
This also gives us some CSRF protection. Note that we make use of the
hmac to protect the redirect value.
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
Diffstat (limited to 'filters')
-rw-r--r-- | filters/simple-authentication.lua | 200 |
1 files changed, 121 insertions, 79 deletions
diff --git a/filters/simple-authentication.lua b/filters/simple-authentication.lua index 4cd4983..5935d08 100644 --- a/filters/simple-authentication.lua +++ b/filters/simple-authentication.lua | |||
@@ -33,15 +33,28 @@ local secret = "BE SURE TO CUSTOMIZE THIS STRING TO SOMETHING BIG AND RANDOM" | |||
33 | -- | 33 | -- |
34 | -- | 34 | -- |
35 | 35 | ||
36 | -- Sets HTTP cookie headers based on post | 36 | -- Sets HTTP cookie headers based on post and sets up redirection. |
37 | function authenticate_post() | 37 | function authenticate_post() |
38 | local password = users[post["username"]] | 38 | local password = users[post["username"]] |
39 | -- TODO: Implement time invariant string comparison function to mitigate against timing attack. | 39 | local redirect = validate_value(post["redirect"]) |
40 | |||
41 | if redirect == nil then | ||
42 | not_found() | ||
43 | return 0 | ||
44 | end | ||
45 | |||
46 | redirect_to(redirect) | ||
47 | |||
48 | -- TODO: Implement time invariant string comparison function to mitigate timing attack. | ||
40 | if password == nil or password ~= post["password"] then | 49 | if password == nil or password ~= post["password"] then |
41 | construct_cookie("", "cgitauth") | 50 | set_cookie("cgitauth", "") |
42 | else | 51 | else |
43 | construct_cookie(post["username"], "cgitauth") | 52 | -- One week expiration time |
53 | local username = secure_value(post["username"], os.time() + 604800) | ||
54 | set_cookie("cgitauth", username) | ||
44 | end | 55 | end |
56 | |||
57 | html("\n") | ||
45 | return 0 | 58 | return 0 |
46 | end | 59 | end |
47 | 60 | ||
@@ -54,8 +67,8 @@ function authenticate_cookie() | |||
54 | return 1 | 67 | return 1 |
55 | end | 68 | end |
56 | 69 | ||
57 | local username = validate_cookie(get_cookie(http["cookie"], "cgitauth")) | 70 | local username = validate_value(get_cookie(http["cookie"], "cgitauth")) |
58 | if username == nil or not accepted_users[username] then | 71 | if username == nil or not accepted_users[username:lower()] then |
59 | return 0 | 72 | return 0 |
60 | else | 73 | else |
61 | return 1 | 74 | return 1 |
@@ -68,6 +81,9 @@ function body() | |||
68 | html("<form method='post' action='") | 81 | html("<form method='post' action='") |
69 | html_attr(cgit["login"]) | 82 | html_attr(cgit["login"]) |
70 | html("'>") | 83 | html("'>") |
84 | html("<input type='hidden' name='redirect' value='") | ||
85 | html_attr(secure_value(cgit["url"], 0)) | ||
86 | html("' />") | ||
71 | html("<table>") | 87 | html("<table>") |
72 | html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>") | 88 | html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>") |
73 | html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>") | 89 | html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>") |
@@ -78,81 +94,10 @@ function body() | |||
78 | end | 94 | end |
79 | 95 | ||
80 | 96 | ||
81 | -- | ||
82 | -- | ||
83 | -- Cookie construction and validation helpers. | ||
84 | -- | ||
85 | -- | ||
86 | |||
87 | local crypto = require("crypto") | ||
88 | |||
89 | -- Returns username of cookie if cookie is valid. Otherwise returns nil. | ||
90 | function validate_cookie(cookie) | ||
91 | local i = 0 | ||
92 | local username = "" | ||
93 | local expiration = 0 | ||
94 | local salt = "" | ||
95 | local hmac = "" | ||
96 | |||
97 | if cookie:len() < 3 or cookie:sub(1, 1) == "|" then | ||
98 | return nil | ||
99 | end | ||
100 | |||
101 | for component in string.gmatch(cookie, "[^|]+") do | ||
102 | if i == 0 then | ||
103 | username = component | ||
104 | elseif i == 1 then | ||
105 | expiration = tonumber(component) | ||
106 | if expiration == nil then | ||
107 | expiration = 0 | ||
108 | end | ||
109 | elseif i == 2 then | ||
110 | salt = component | ||
111 | elseif i == 3 then | ||
112 | hmac = component | ||
113 | else | ||
114 | break | ||
115 | end | ||
116 | i = i + 1 | ||
117 | end | ||
118 | |||
119 | if hmac == nil or hmac:len() == 0 then | ||
120 | return nil | ||
121 | end | ||
122 | |||
123 | -- TODO: implement time invariant comparison to prevent against timing attack. | ||
124 | if hmac ~= crypto.hmac.digest("sha1", username .. "|" .. tostring(expiration) .. "|" .. salt, secret) then | ||
125 | return nil | ||
126 | end | ||
127 | |||
128 | if expiration <= os.time() then | ||
129 | return nil | ||
130 | end | ||
131 | |||
132 | return username:lower() | ||
133 | end | ||
134 | |||
135 | function construct_cookie(username, cookie) | ||
136 | local authstr = "" | ||
137 | if username:len() > 0 then | ||
138 | -- One week expiration time | ||
139 | local expiration = os.time() + 604800 | ||
140 | local salt = crypto.hex(crypto.rand.bytes(16)) | ||
141 | |||
142 | authstr = username .. "|" .. tostring(expiration) .. "|" .. salt | ||
143 | authstr = authstr .. "|" .. crypto.hmac.digest("sha1", authstr, secret) | ||
144 | end | ||
145 | |||
146 | html("Set-Cookie: " .. cookie .. "=" .. authstr .. "; HttpOnly") | ||
147 | if http["https"] == "yes" or http["https"] == "on" or http["https"] == "1" then | ||
148 | html("; secure") | ||
149 | end | ||
150 | html("\n") | ||
151 | end | ||
152 | 97 | ||
153 | -- | 98 | -- |
154 | -- | 99 | -- |
155 | -- Wrapper around filter API follows below, exposing the http table, the cgit table, and the post table to the above functions. | 100 | -- Wrapper around filter API, exposing the http table, the cgit table, and the post table to the above functions. |
156 | -- | 101 | -- |
157 | -- | 102 | -- |
158 | 103 | ||
@@ -197,7 +142,7 @@ end | |||
197 | 142 | ||
198 | -- | 143 | -- |
199 | -- | 144 | -- |
200 | -- Utility functions follow below, based on keplerproject/wsapi. | 145 | -- Utility functions based on keplerproject/wsapi. |
201 | -- | 146 | -- |
202 | -- | 147 | -- |
203 | 148 | ||
@@ -211,6 +156,16 @@ function url_decode(str) | |||
211 | return str | 156 | return str |
212 | end | 157 | end |
213 | 158 | ||
159 | function url_encode(str) | ||
160 | if not str then | ||
161 | return "" | ||
162 | end | ||
163 | str = string.gsub(str, "\n", "\r\n") | ||
164 | str = string.gsub(str, "([^%w ])", function (c) return string.format("%%%02X", string.byte(c)) end) | ||
165 | str = string.gsub(str, " ", "+") | ||
166 | return str | ||
167 | end | ||
168 | |||
214 | function parse_qs(qs) | 169 | function parse_qs(qs) |
215 | local tab = {} | 170 | local tab = {} |
216 | for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do | 171 | for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do |
@@ -223,3 +178,90 @@ function get_cookie(cookies, name) | |||
223 | cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";") | 178 | cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";") |
224 | return url_decode(string.match(cookies, ";" .. name .. "=(.-);")) | 179 | return url_decode(string.match(cookies, ";" .. name .. "=(.-);")) |
225 | end | 180 | end |
181 | |||
182 | |||
183 | -- | ||
184 | -- | ||
185 | -- Cookie construction and validation helpers. | ||
186 | -- | ||
187 | -- | ||
188 | |||
189 | local crypto = require("crypto") | ||
190 | |||
191 | -- Returns value of cookie if cookie is valid. Otherwise returns nil. | ||
192 | function validate_value(cookie) | ||
193 | local i = 0 | ||
194 | local value = "" | ||
195 | local expiration = 0 | ||
196 | local salt = "" | ||
197 | local hmac = "" | ||
198 | |||
199 | if cookie == nil or cookie:len() < 3 or cookie:sub(1, 1) == "|" then | ||
200 | return nil | ||
201 | end | ||
202 | |||
203 | for component in string.gmatch(cookie, "[^|]+") do | ||
204 | if i == 0 then | ||
205 | value = component | ||
206 | elseif i == 1 then | ||
207 | expiration = tonumber(component) | ||
208 | if expiration == nil then | ||
209 | expiration = 0 | ||
210 | end | ||
211 | elseif i == 2 then | ||
212 | salt = component | ||
213 | elseif i == 3 then | ||
214 | hmac = component | ||
215 | else | ||
216 | break | ||
217 | end | ||
218 | i = i + 1 | ||
219 | end | ||
220 | |||
221 | if hmac == nil or hmac:len() == 0 then | ||
222 | return nil | ||
223 | end | ||
224 | |||
225 | -- TODO: implement time invariant comparison to prevent against timing attack. | ||
226 | if hmac ~= crypto.hmac.digest("sha1", value .. "|" .. tostring(expiration) .. "|" .. salt, secret) then | ||
227 | return nil | ||
228 | end | ||
229 | |||
230 | if expiration ~= 0 and expiration <= os.time() then | ||
231 | return nil | ||
232 | end | ||
233 | |||
234 | return url_decode(value) | ||
235 | end | ||
236 | |||
237 | function secure_value(value, expiration) | ||
238 | if value == nil or value:len() <= 0 then | ||
239 | return "" | ||
240 | end | ||
241 | |||
242 | local authstr = "" | ||
243 | local salt = crypto.hex(crypto.rand.bytes(16)) | ||
244 | value = url_encode(value) | ||
245 | authstr = value .. "|" .. tostring(expiration) .. "|" .. salt | ||
246 | authstr = authstr .. "|" .. crypto.hmac.digest("sha1", authstr, secret) | ||
247 | return authstr | ||
248 | end | ||
249 | |||
250 | function set_cookie(cookie, value) | ||
251 | html("Set-Cookie: " .. cookie .. "=" .. value .. "; HttpOnly") | ||
252 | if http["https"] == "yes" or http["https"] == "on" or http["https"] == "1" then | ||
253 | html("; secure") | ||
254 | end | ||
255 | html("\n") | ||
256 | end | ||
257 | |||
258 | function redirect_to(url) | ||
259 | html("Status: 302 Redirect\n") | ||
260 | html("Cache-Control: no-cache, no-store\n") | ||
261 | html("Location: " .. url .. "\n") | ||
262 | end | ||
263 | |||
264 | function not_found() | ||
265 | html("Status: 404 Not Found\n") | ||
266 | html("Cache-Control: no-cache, no-store\n\n") | ||
267 | end | ||