about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorJason A. Donenfeld2014-01-16 11:39:17 +0100
committerJason A. Donenfeld2014-01-16 12:13:39 +0100
commitb826537cb4aa2358027ffcb1dd6a87274734e962 (patch)
tree7c749c66d868cb996828d2b65a4bede58b5ebd62
parentauth: add basic authentication filter framework (diff)
downloadcgit-b826537cb4aa2358027ffcb1dd6a87274734e962.tar.gz
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>
-rw-r--r--cgit.c22
-rw-r--r--cgitrc.5.txt3
-rw-r--r--filters/simple-authentication.lua200
3 files changed, 131 insertions, 94 deletions
diff --git a/cgit.c b/cgit.c index c52ef33..be1265d 100644 --- a/cgit.c +++ b/cgit.c
@@ -614,22 +614,19 @@ static inline void open_auth_filter(struct cgit_context *ctx, const char *functi
614 ctx->qry.url ? ctx->qry.url : ""); 614 ctx->qry.url ? ctx->qry.url : "");
615} 615}
616 616
617/* We intentionally keep this rather small, instead of looping and
618 * feeding it to the filter a couple bytes at a time. This way, the
619 * filter itself does not need to handle any denial of service or
620 * buffer bloat issues. If this winds up being too small, people
621 * will complain on the mailing list, and we'll increase it as needed. */
617#define MAX_AUTHENTICATION_POST_BYTES 4096 622#define MAX_AUTHENTICATION_POST_BYTES 4096
623/* The filter is expected to spit out "Status: " and all headers. */
618static inline void authenticate_post(struct cgit_context *ctx) 624static inline void authenticate_post(struct cgit_context *ctx)
619{ 625{
620 if (ctx->env.http_referer && strlen(ctx->env.http_referer) > 0) {
621 html("Status: 302 Redirect\n");
622 html("Cache-Control: no-cache, no-store\n");
623 htmlf("Location: %s\n", ctx->env.http_referer);
624 } else {
625 html("Status: 501 Missing Referer\n");
626 html("Cache-Control: no-cache, no-store\n\n");
627 exit(0);
628 }
629
630 open_auth_filter(ctx, "authenticate-post");
631 char buffer[MAX_AUTHENTICATION_POST_BYTES]; 626 char buffer[MAX_AUTHENTICATION_POST_BYTES];
632 int len; 627 int len;
628
629 open_auth_filter(ctx, "authenticate-post");
633 len = ctx->env.content_length; 630 len = ctx->env.content_length;
634 if (len > MAX_AUTHENTICATION_POST_BYTES) 631 if (len > MAX_AUTHENTICATION_POST_BYTES)
635 len = MAX_AUTHENTICATION_POST_BYTES; 632 len = MAX_AUTHENTICATION_POST_BYTES;
@@ -637,10 +634,7 @@ static inline void authenticate_post(struct cgit_context *ctx)
637 die_errno("Could not read POST from stdin"); 634 die_errno("Could not read POST from stdin");
638 if (write(STDOUT_FILENO, buffer, len) < 0) 635 if (write(STDOUT_FILENO, buffer, len) < 0)
639 die_errno("Could not write POST to stdout"); 636 die_errno("Could not write POST to stdout");
640 /* The filter may now spit out a Set-Cookie: ... */
641 cgit_close_filter(ctx->cfg.auth_filter); 637 cgit_close_filter(ctx->cfg.auth_filter);
642
643 html("\n");
644 exit(0); 638 exit(0);
645} 639}
646 640
diff --git a/cgitrc.5.txt b/cgitrc.5.txt index c45dbd3..682d8bb 100644 --- a/cgitrc.5.txt +++ b/cgitrc.5.txt
@@ -662,7 +662,8 @@ auth filter::
662 the http cookie and return a 0 if it is invalid or 1 if it is invalid, 662 the http cookie and return a 0 if it is invalid or 1 if it is invalid,
663 in the exit code / close function. If the filter action is 663 in the exit code / close function. If the filter action is
664 "authenticate-post", this filter receives POST'd parameters on 664 "authenticate-post", this filter receives POST'd parameters on
665 standard input, and should write to output one or more "Set-Cookie" 665 standard input, and should write a complete CGI request, preferably
666 with a 302 redirect, and write to output one or more "Set-Cookie"
666 HTTP headers, each followed by a newline. 667 HTTP headers, each followed by a newline.
667 668
668 Please see `filters/simple-authentication.lua` for a clear example 669 Please see `filters/simple-authentication.lua` for a clear example
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.
37function authenticate_post() 37function 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
46end 59end
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()
78end 94end
79 95
80 96
81--
82--
83-- Cookie construction and validation helpers.
84--
85--
86
87local crypto = require("crypto")
88
89-- Returns username of cookie if cookie is valid. Otherwise returns nil.
90function 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()
133end
134
135function 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")
151end
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
212end 157end
213 158
159function 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
167end
168
214function parse_qs(qs) 169function 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 .. "=(.-);"))
225end 180end
181
182
183--
184--
185-- Cookie construction and validation helpers.
186--
187--
188
189local crypto = require("crypto")
190
191-- Returns value of cookie if cookie is valid. Otherwise returns nil.
192function 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)
235end
236
237function 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
248end
249
250function 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")
256end
257
258function redirect_to(url)
259 html("Status: 302 Redirect\n")
260 html("Cache-Control: no-cache, no-store\n")
261 html("Location: " .. url .. "\n")
262end
263
264function not_found()
265 html("Status: 404 Not Found\n")
266 html("Cache-Control: no-cache, no-store\n\n")
267end