diff options
| -rw-r--r-- | cgit.c | 22 | ||||
| -rw-r--r-- | cgitrc.5.txt | 3 | ||||
| -rw-r--r-- | filters/simple-authentication.lua | 200 |
3 files changed, 131 insertions, 94 deletions
| @@ -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. */ | ||
| 618 | static inline void authenticate_post(struct cgit_context *ctx) | 624 | static 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. |
| 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 | ||
