Your one-stop-cake-shop for everything Freshly Baked has to offer

fix(m/security): block Cross-Site Request Forgery #188

closed opened by a.starrysky.fyi targeting main from private/minion/push-xpqrvpwlrtsk

We were previously vulnerable to cross-site request forgery: someone giving us a link that ran an action from somewhere else. To fix this, we can tie a token which is sent along with all our actions to a session. That way, an attacker won't know the correct token to run an action on behalf of a user

Labels

None yet.

requested-reviewers

None yet.

approved

None yet.

tested-working

None yet.

rejected

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:uuyqs6y3pwtbteet4swt5i5y/sh.tangled.repo.pull/3mdi2nvgvon22
+304 -15
Diff #5
+210 -4
menu/Cargo.lock
··· 17 source = "registry+https://github.com/rust-lang/crates.io-index" 18 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 19 20 [[package]] 21 name = "atoi" 22 version = "2.0.0" ··· 181 source = "registry+https://github.com/rust-lang/crates.io-index" 182 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 183 184 [[package]] 185 name = "cpufeatures" 186 version = "0.2.17" ··· 241 "zeroize", 242 ] 243 244 [[package]] 245 name = "digest" 246 version = "0.10.7" ··· 339 "percent-encoding", 340 ] 341 342 [[package]] 343 name = "futures-channel" 344 version = "0.3.31" ··· 383 source = "registry+https://github.com/rust-lang/crates.io-index" 384 checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 385 386 [[package]] 387 name = "futures-sink" 388 version = "0.3.31" ··· 403 dependencies = [ 404 "futures-core", 405 "futures-io", 406 "futures-sink", 407 "futures-task", 408 "memchr", ··· 432 "wasi", 433 ] 434 435 [[package]] 436 name = "hashbrown" 437 version = "0.15.5" ··· 796 checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 797 dependencies = [ 798 "scopeguard", 799 ] 800 801 [[package]] ··· 843 "tower-http", 844 "tower-layer", 845 "tower-serve-static", 846 ] 847 848 [[package]] ··· 888 "zeroize", 889 ] 890 891 [[package]] 892 name = "num-integer" 893 version = "0.1.46" ··· 1036 "zerovec", 1037 ] 1038 1039 [[package]] 1040 name = "ppv-lite86" 1041 version = "0.2.21" ··· 1063 "proc-macro2", 1064 ] 1065 1066 [[package]] 1067 name = "rand" 1068 version = "0.8.5" ··· 1090 source = "registry+https://github.com/rust-lang/crates.io-index" 1091 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1092 dependencies = [ 1093 - "getrandom", 1094 ] 1095 1096 [[package]] ··· 1188 dependencies = [ 1189 "cc", 1190 "cfg-if", 1191 - "getrandom", 1192 "libc", 1193 "untrusted", 1194 "windows-sys 0.52.0", ··· 1690 "syn 2.0.112", 1691 ] 1692 1693 [[package]] 1694 name = "tinystr" 1695 version = "0.8.2" ··· 1781 "tracing", 1782 ] 1783 1784 [[package]] 1785 name = "tower-http" 1786 version = "0.6.8" ··· 1840 source = "registry+https://github.com/rust-lang/crates.io-index" 1841 checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1842 1843 [[package]] 1844 name = "tracing" 1845 version = "0.1.44" ··· 1943 1944 [[package]] 1945 name = "uuid" 1946 - version = "1.19.0" 1947 source = "registry+https://github.com/rust-lang/crates.io-index" 1948 - checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" 1949 dependencies = [ 1950 "js-sys", 1951 "wasm-bindgen", 1952 ] 1953 ··· 1969 source = "registry+https://github.com/rust-lang/crates.io-index" 1970 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1971 1972 [[package]] 1973 name = "wasite" 1974 version = "0.1.0" ··· 2276 source = "registry+https://github.com/rust-lang/crates.io-index" 2277 checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 2278 2279 [[package]] 2280 name = "writeable" 2281 version = "0.6.2"
··· 17 source = "registry+https://github.com/rust-lang/crates.io-index" 18 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 19 20 + [[package]] 21 + name = "async-trait" 22 + version = "0.1.89" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" 25 + dependencies = [ 26 + "proc-macro2", 27 + "quote", 28 + "syn 2.0.112", 29 + ] 30 + 31 [[package]] 32 name = "atoi" 33 version = "2.0.0" ··· 192 source = "registry+https://github.com/rust-lang/crates.io-index" 193 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 194 195 + [[package]] 196 + name = "cookie" 197 + version = "0.18.1" 198 + source = "registry+https://github.com/rust-lang/crates.io-index" 199 + checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" 200 + dependencies = [ 201 + "percent-encoding", 202 + "time", 203 + "version_check", 204 + ] 205 + 206 [[package]] 207 name = "cpufeatures" 208 version = "0.2.17" ··· 263 "zeroize", 264 ] 265 266 + [[package]] 267 + name = "deranged" 268 + version = "0.5.5" 269 + source = "registry+https://github.com/rust-lang/crates.io-index" 270 + checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 271 + dependencies = [ 272 + "powerfmt", 273 + "serde_core", 274 + ] 275 + 276 [[package]] 277 name = "digest" 278 version = "0.10.7" ··· 371 "percent-encoding", 372 ] 373 374 + [[package]] 375 + name = "futures" 376 + version = "0.3.31" 377 + source = "registry+https://github.com/rust-lang/crates.io-index" 378 + checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 379 + dependencies = [ 380 + "futures-channel", 381 + "futures-core", 382 + "futures-io", 383 + "futures-sink", 384 + "futures-task", 385 + "futures-util", 386 + ] 387 + 388 [[package]] 389 name = "futures-channel" 390 version = "0.3.31" ··· 429 source = "registry+https://github.com/rust-lang/crates.io-index" 430 checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 431 432 + [[package]] 433 + name = "futures-macro" 434 + version = "0.3.31" 435 + source = "registry+https://github.com/rust-lang/crates.io-index" 436 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 437 + dependencies = [ 438 + "proc-macro2", 439 + "quote", 440 + "syn 2.0.112", 441 + ] 442 + 443 [[package]] 444 name = "futures-sink" 445 version = "0.3.31" ··· 460 dependencies = [ 461 "futures-core", 462 "futures-io", 463 + "futures-macro", 464 "futures-sink", 465 "futures-task", 466 "memchr", ··· 490 "wasi", 491 ] 492 493 + [[package]] 494 + name = "getrandom" 495 + version = "0.3.4" 496 + source = "registry+https://github.com/rust-lang/crates.io-index" 497 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 498 + dependencies = [ 499 + "cfg-if", 500 + "libc", 501 + "r-efi", 502 + "wasip2", 503 + ] 504 + 505 [[package]] 506 name = "hashbrown" 507 version = "0.15.5" ··· 866 checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 867 dependencies = [ 868 "scopeguard", 869 + "serde", 870 ] 871 872 [[package]] ··· 914 "tower-http", 915 "tower-layer", 916 "tower-serve-static", 917 + "tower-sessions", 918 + "uuid", 919 ] 920 921 [[package]] ··· 961 "zeroize", 962 ] 963 964 + [[package]] 965 + name = "num-conv" 966 + version = "0.2.0" 967 + source = "registry+https://github.com/rust-lang/crates.io-index" 968 + checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" 969 + 970 [[package]] 971 name = "num-integer" 972 version = "0.1.46" ··· 1115 "zerovec", 1116 ] 1117 1118 + [[package]] 1119 + name = "powerfmt" 1120 + version = "0.2.0" 1121 + source = "registry+https://github.com/rust-lang/crates.io-index" 1122 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1123 + 1124 [[package]] 1125 name = "ppv-lite86" 1126 version = "0.2.21" ··· 1148 "proc-macro2", 1149 ] 1150 1151 + [[package]] 1152 + name = "r-efi" 1153 + version = "5.3.0" 1154 + source = "registry+https://github.com/rust-lang/crates.io-index" 1155 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 1156 + 1157 [[package]] 1158 name = "rand" 1159 version = "0.8.5" ··· 1181 source = "registry+https://github.com/rust-lang/crates.io-index" 1182 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1183 dependencies = [ 1184 + "getrandom 0.2.16", 1185 ] 1186 1187 [[package]] ··· 1279 dependencies = [ 1280 "cc", 1281 "cfg-if", 1282 + "getrandom 0.2.16", 1283 "libc", 1284 "untrusted", 1285 "windows-sys 0.52.0", ··· 1781 "syn 2.0.112", 1782 ] 1783 1784 + [[package]] 1785 + name = "time" 1786 + version = "0.3.46" 1787 + source = "registry+https://github.com/rust-lang/crates.io-index" 1788 + checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" 1789 + dependencies = [ 1790 + "deranged", 1791 + "itoa", 1792 + "num-conv", 1793 + "powerfmt", 1794 + "serde_core", 1795 + "time-core", 1796 + "time-macros", 1797 + ] 1798 + 1799 + [[package]] 1800 + name = "time-core" 1801 + version = "0.1.8" 1802 + source = "registry+https://github.com/rust-lang/crates.io-index" 1803 + checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" 1804 + 1805 + [[package]] 1806 + name = "time-macros" 1807 + version = "0.2.26" 1808 + source = "registry+https://github.com/rust-lang/crates.io-index" 1809 + checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" 1810 + dependencies = [ 1811 + "num-conv", 1812 + "time-core", 1813 + ] 1814 + 1815 [[package]] 1816 name = "tinystr" 1817 version = "0.8.2" ··· 1903 "tracing", 1904 ] 1905 1906 + [[package]] 1907 + name = "tower-cookies" 1908 + version = "0.11.0" 1909 + source = "registry+https://github.com/rust-lang/crates.io-index" 1910 + checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36" 1911 + dependencies = [ 1912 + "axum-core", 1913 + "cookie", 1914 + "futures-util", 1915 + "http", 1916 + "parking_lot", 1917 + "pin-project-lite", 1918 + "tower-layer", 1919 + "tower-service", 1920 + ] 1921 + 1922 [[package]] 1923 name = "tower-http" 1924 version = "0.6.8" ··· 1978 source = "registry+https://github.com/rust-lang/crates.io-index" 1979 checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1980 1981 + [[package]] 1982 + name = "tower-sessions" 1983 + version = "0.14.0" 1984 + source = "registry+https://github.com/rust-lang/crates.io-index" 1985 + checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3" 1986 + dependencies = [ 1987 + "async-trait", 1988 + "http", 1989 + "time", 1990 + "tokio", 1991 + "tower-cookies", 1992 + "tower-layer", 1993 + "tower-service", 1994 + "tower-sessions-core", 1995 + "tower-sessions-memory-store", 1996 + "tracing", 1997 + ] 1998 + 1999 + [[package]] 2000 + name = "tower-sessions-core" 2001 + version = "0.14.0" 2002 + source = "registry+https://github.com/rust-lang/crates.io-index" 2003 + checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b" 2004 + dependencies = [ 2005 + "async-trait", 2006 + "axum-core", 2007 + "base64", 2008 + "futures", 2009 + "http", 2010 + "parking_lot", 2011 + "rand", 2012 + "serde", 2013 + "serde_json", 2014 + "thiserror", 2015 + "time", 2016 + "tokio", 2017 + "tracing", 2018 + ] 2019 + 2020 + [[package]] 2021 + name = "tower-sessions-memory-store" 2022 + version = "0.14.0" 2023 + source = "registry+https://github.com/rust-lang/crates.io-index" 2024 + checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242" 2025 + dependencies = [ 2026 + "async-trait", 2027 + "time", 2028 + "tokio", 2029 + "tower-sessions-core", 2030 + ] 2031 + 2032 [[package]] 2033 name = "tracing" 2034 version = "0.1.44" ··· 2132 2133 [[package]] 2134 name = "uuid" 2135 + version = "1.20.0" 2136 source = "registry+https://github.com/rust-lang/crates.io-index" 2137 + checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" 2138 dependencies = [ 2139 + "getrandom 0.3.4", 2140 "js-sys", 2141 + "serde_core", 2142 "wasm-bindgen", 2143 ] 2144 ··· 2160 source = "registry+https://github.com/rust-lang/crates.io-index" 2161 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 2162 2163 + [[package]] 2164 + name = "wasip2" 2165 + version = "1.0.2+wasi-0.2.9" 2166 + source = "registry+https://github.com/rust-lang/crates.io-index" 2167 + checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" 2168 + dependencies = [ 2169 + "wit-bindgen", 2170 + ] 2171 + 2172 [[package]] 2173 name = "wasite" 2174 version = "0.1.0" ··· 2476 source = "registry+https://github.com/rust-lang/crates.io-index" 2477 checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 2478 2479 + [[package]] 2480 + name = "wit-bindgen" 2481 + version = "0.51.0" 2482 + source = "registry+https://github.com/rust-lang/crates.io-index" 2483 + checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" 2484 + 2485 [[package]] 2486 name = "writeable" 2487 version = "0.6.2"
+2
menu/Cargo.toml
··· 21 tower-http = { version = "0.6.8", features = ["fs", "normalize-path"] } 22 tower-layer = "0.3.3" 23 tower-serve-static = "0.1.1"
··· 21 tower-http = { version = "0.6.8", features = ["fs", "normalize-path"] } 22 tower-layer = "0.3.3" 23 tower-serve-static = "0.1.1" 24 + tower-sessions = "0.14.0" 25 + uuid = { version = "1.20.0", features = ["v4", "serde"] }
+1
menu/src/html/create.html
··· 25 </div> 26 </div> 27 <input type="hidden" name="current" id="current" value="{current:attribute}" /> 28 <input type="submit" value="Add shortlink" /> 29 </form> 30 </body>
··· 25 </div> 26 </div> 27 <input type="hidden" name="current" id="current" value="{current:attribute}" /> 28 + <input type="hidden" name="token" id="token" value="{token:attribute}" /> 29 <input type="submit" value="Add shortlink" /> 30 </form> 31 </body>
+1 -1
menu/src/html/create/conflict.html
··· 16 <div id="logo"><div><span><span class="failure">Couldn't create</span> shortlink</span><img src="/_/public/logo.svg"></div></div> 17 <p>You can't create a link from <a href="/{from:url}">{host}/{from}</a> to <a href="{to:attribute}">{to}</a> because <a href="/{from:url}">{host}/{from}</a> already goes to <a href="{current:attribute}">{current}</a>.</p> 18 <ul> 19 - <li><a href="/_/create/do?from={from:url}&to={to:url}&current={current:url}">Overwrite it</a></li> 20 <li><a href="/_/create?from={from:url}&to={to:url}">Back to editor</a></li> 21 <li><a href="/">All shortlinks</a></li> 22 </ul>
··· 16 <div id="logo"><div><span><span class="failure">Couldn't create</span> shortlink</span><img src="/_/public/logo.svg"></div></div> 17 <p>You can't create a link from <a href="/{from:url}">{host}/{from}</a> to <a href="{to:attribute}">{to}</a> because <a href="/{from:url}">{host}/{from}</a> already goes to <a href="{current:attribute}">{current}</a>.</p> 18 <ul> 19 + <li><a href="/_/create/do?from={from:url}&to={to:url}&current={current:url}&token={token:url}">Overwrite it</a></li> 20 <li><a href="/_/create?from={from:url}&to={to:url}">Back to editor</a></li> 21 <li><a href="/">All shortlinks</a></li> 22 </ul>
+1 -1
menu/src/html/create/success.html
··· 20 <li>Access it at <a href="/{from:url}">{host}/{from}</a> (<span id="copy" text="{host:attribute}/{from:attribute}">copy</span>)</li> 21 <li><a href="/_/create">Create another one</a></li> 22 <li><a href="/_/create?from={from:url}&to={to:url}&current={to:url}">Edit it</a></li> 23 - <li><a href="/_/delete/do?from={from:url}&current={to:url}">Delete it</a></li> 24 <li><a href="/">All shortlinks</a></li> 25 </ul> 26 <script>
··· 20 <li>Access it at <a href="/{from:url}">{host}/{from}</a> (<span id="copy" text="{host:attribute}/{from:attribute}">copy</span>)</li> 21 <li><a href="/_/create">Create another one</a></li> 22 <li><a href="/_/create?from={from:url}&to={to:url}&current={to:url}">Edit it</a></li> 23 + <li><a href="/_/delete/do?from={from:url}&current={to:url}&token={token:url}">Delete it</a></li> 24 <li><a href="/">All shortlinks</a></li> 25 </ul> 26 <script>
+89 -9
menu/src/main.rs
··· 13 use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode}; 14 use regex::Captures; 15 use sqlx::{Connection, PgConnection}; 16 17 #[cfg(debug_assertions)] 18 use std::fs; ··· 31 #[cfg(debug_assertions)] 32 static DEVELOPMENT: OnceLock<bool> = OnceLock::new(); 33 34 #[derive(Clone)] 35 enum AnyString<'a> { 36 Owned(String), ··· 149 150 #[axum::debug_handler] 151 async fn handle_index( 152 headers: HeaderMap, 153 Query(params): Query<HashMap<String, String>>, 154 ) -> Result<Html<String>> { 155 - handle_static_page(StaticPageType::Index, &params, &headers).await 156 } 157 158 async fn handle_base(Path(go): Path<String>) -> Redirect { ··· 186 187 async fn handle_static_page<'a>( 188 page_type: StaticPageType, 189 params: &'a HashMap<String, String>, 190 headers: &'a HeaderMap, 191 ) -> Result<Html<String>> { ··· 255 Box::new(move || username.and_then(|name| Some(AnyString::Ref(name)))), 256 ); 257 258 if matches!(page_type, StaticPageType::Index) { 259 let links_query = sqlx::query_as!( 260 Link, ··· 291 <td><a href="{from_attribute}">{from}</a></td> 292 <td><a href="{to_attribute}">{to}</a></td> 293 <td>{owner}</td> 294 - <td>(<a href="/_/create?from={from_url}&to={to_url}&current={to_url}">edit</a>) (<a href="/_/delete/do?from={from_url}&current={to_url}">delete</a>)</td> 295 </tr>"#, 296 )); 297 } ··· 303 ); 304 } 305 306 let result = template_html(html, replacements); 307 Ok(Html(result)) 308 } 309 310 async fn handle_create_page( 311 Query(params): Query<HashMap<String, String>>, 312 headers: HeaderMap, 313 ) -> Result<Html<String>> { 314 - handle_static_page(StaticPageType::Create, &params, &headers).await 315 } 316 async fn handle_create_success_page( 317 Query(params): Query<HashMap<String, String>>, 318 headers: HeaderMap, 319 ) -> Result<Html<String>> { 320 - handle_static_page(StaticPageType::CreateSuccess, &params, &headers).await 321 } 322 async fn handle_create_conflict_page( 323 Query(params): Query<HashMap<String, String>>, 324 headers: HeaderMap, 325 ) -> Result<Html<String>> { 326 - handle_static_page(StaticPageType::CreateConflict, &params, &headers).await 327 } 328 async fn handle_create_failure_page( 329 Query(params): Query<HashMap<String, String>>, 330 headers: HeaderMap, 331 ) -> Result<impl IntoResponse> { 332 - handle_static_page(StaticPageType::CreateFailure, &params, &headers) 333 .await 334 .and_then(|html| Ok((StatusCode::INTERNAL_SERVER_ERROR, html))) 335 } 336 async fn handle_delete_success_page( 337 Query(params): Query<HashMap<String, String>>, 338 headers: HeaderMap, 339 ) -> Result<Html<String>> { 340 - handle_static_page(StaticPageType::DeleteSuccess, &params, &headers).await 341 } 342 async fn handle_delete_failure_page( 343 Query(params): Query<HashMap<String, String>>, 344 headers: HeaderMap, 345 ) -> Result<Html<String>> { 346 - handle_static_page(StaticPageType::DeleteFailure, &params, &headers).await 347 } 348 349 struct NotAuthenticated; ··· 353 } 354 } 355 356 fn ensure_authenticated<'a>( 357 headers: &'a HeaderMap, 358 #[cfg(debug_assertions)] params: &'a HashMap<String, String>, ··· 376 Err(NotAuthenticated {}.into()) 377 } 378 379 #[axum::debug_handler] 380 async fn handle_create_do( 381 headers: HeaderMap, 382 Query(params): Query<HashMap<String, String>>, 383 ) -> Result<Response<Body>> { 384 let owner = ensure_authenticated( 385 &headers, 386 #[cfg(debug_assertions)] ··· 447 448 #[axum::debug_handler] 449 async fn handle_delete_do( 450 headers: HeaderMap, 451 Query(params): Query<HashMap<String, String>>, 452 ) -> Result<Response<Body>> { 453 ensure_authenticated( 454 &headers, 455 #[cfg(debug_assertions)] ··· 534 .expect("Failed to connect to database defined in $DATABASE_URL after 3 retries") 535 }; 536 537 sqlx::migrate!() 538 .run(&mut connection) 539 .await ··· 578 .route("/_/search", get(handle_search)) 579 .route("/_/{*route}", get(handle_404)) 580 .route("/{*go}", get(handle_base)); 581 - let app = NormalizePathLayer::trim_trailing_slash().layer(router); 582 583 let listener = tokio::net::TcpListener::bind( 584 env::var("BIND_ADDR").unwrap_or_else(|_| "0.0.0.0:3000".to_string()),
··· 13 use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode}; 14 use regex::Captures; 15 use sqlx::{Connection, PgConnection}; 16 + use tower_sessions::{MemoryStore, Session, SessionManagerLayer}; 17 + use uuid::Uuid; 18 19 #[cfg(debug_assertions)] 20 use std::fs; ··· 33 #[cfg(debug_assertions)] 34 static DEVELOPMENT: OnceLock<bool> = OnceLock::new(); 35 36 + const TOKEN_KEY: &str = "token"; 37 + 38 #[derive(Clone)] 39 enum AnyString<'a> { 40 Owned(String), ··· 153 154 #[axum::debug_handler] 155 async fn handle_index( 156 + session: Session, 157 headers: HeaderMap, 158 Query(params): Query<HashMap<String, String>>, 159 ) -> Result<Html<String>> { 160 + handle_static_page(StaticPageType::Index, session, &params, &headers).await 161 } 162 163 async fn handle_base(Path(go): Path<String>) -> Redirect { ··· 191 192 async fn handle_static_page<'a>( 193 page_type: StaticPageType, 194 + session: Session, 195 params: &'a HashMap<String, String>, 196 headers: &'a HeaderMap, 197 ) -> Result<Html<String>> { ··· 261 Box::new(move || username.and_then(|name| Some(AnyString::Ref(name)))), 262 ); 263 264 + let token: String = { 265 + let maybe_token = session.get(TOKEN_KEY).await.unwrap(); 266 + if let Some(token) = maybe_token { 267 + token 268 + } else { 269 + let new_token = Uuid::new_v4().to_string(); 270 + session.insert(TOKEN_KEY, &new_token).await.unwrap(); 271 + new_token 272 + } 273 + }; 274 + 275 if matches!(page_type, StaticPageType::Index) { 276 let links_query = sqlx::query_as!( 277 Link, ··· 308 <td><a href="{from_attribute}">{from}</a></td> 309 <td><a href="{to_attribute}">{to}</a></td> 310 <td>{owner}</td> 311 + <td>(<a href="/_/create?from={from_url}&to={to_url}&current={to_url}">edit</a>) (<a href="/_/delete/do?from={from_url}&current={to_url}&token={token}">delete</a>)</td> 312 </tr>"#, 313 )); 314 } ··· 320 ); 321 } 322 323 + replacements.insert( 324 + "token", 325 + Box::new(move || Some(AnyString::Owned(token.clone()))), 326 + ); 327 + 328 let result = template_html(html, replacements); 329 Ok(Html(result)) 330 } 331 332 async fn handle_create_page( 333 + session: Session, 334 Query(params): Query<HashMap<String, String>>, 335 headers: HeaderMap, 336 ) -> Result<Html<String>> { 337 + handle_static_page(StaticPageType::Create, session, &params, &headers).await 338 } 339 async fn handle_create_success_page( 340 + session: Session, 341 Query(params): Query<HashMap<String, String>>, 342 headers: HeaderMap, 343 ) -> Result<Html<String>> { 344 + handle_static_page(StaticPageType::CreateSuccess, session, &params, &headers).await 345 } 346 async fn handle_create_conflict_page( 347 + session: Session, 348 Query(params): Query<HashMap<String, String>>, 349 headers: HeaderMap, 350 ) -> Result<Html<String>> { 351 + handle_static_page(StaticPageType::CreateConflict, session, &params, &headers).await 352 } 353 async fn handle_create_failure_page( 354 + session: Session, 355 Query(params): Query<HashMap<String, String>>, 356 headers: HeaderMap, 357 ) -> Result<impl IntoResponse> { 358 + handle_static_page(StaticPageType::CreateFailure, session, &params, &headers) 359 .await 360 .and_then(|html| Ok((StatusCode::INTERNAL_SERVER_ERROR, html))) 361 } 362 async fn handle_delete_success_page( 363 + session: Session, 364 Query(params): Query<HashMap<String, String>>, 365 headers: HeaderMap, 366 ) -> Result<Html<String>> { 367 + handle_static_page(StaticPageType::DeleteSuccess, session, &params, &headers).await 368 } 369 async fn handle_delete_failure_page( 370 + session: Session, 371 Query(params): Query<HashMap<String, String>>, 372 headers: HeaderMap, 373 ) -> Result<Html<String>> { 374 + handle_static_page(StaticPageType::DeleteFailure, session, &params, &headers).await 375 } 376 377 struct NotAuthenticated; ··· 381 } 382 } 383 384 + struct MissingToken; 385 + impl IntoResponse for MissingToken { 386 + fn into_response(self) -> axum::response::Response { 387 + return ( 388 + StatusCode::FORBIDDEN, 389 + "There's no session here - try going back and trying again?", 390 + ) 391 + .into_response(); 392 + } 393 + } 394 + 395 + struct InvalidToken; 396 + impl IntoResponse for InvalidToken { 397 + fn into_response(self) -> axum::response::Response { 398 + return ( 399 + StatusCode::FORBIDDEN, 400 + "This session is invalid - possible CSRF?", 401 + ) 402 + .into_response(); 403 + } 404 + } 405 + 406 fn ensure_authenticated<'a>( 407 headers: &'a HeaderMap, 408 #[cfg(debug_assertions)] params: &'a HashMap<String, String>, ··· 426 Err(NotAuthenticated {}.into()) 427 } 428 429 + async fn ensure_token<'a>( 430 + session: &Session, 431 + params: &'a HashMap<String, String>, 432 + ) -> Result<(), ErrorResponse> { 433 + let maybe_token: Option<String> = session.get(TOKEN_KEY).await.unwrap(); 434 + 435 + let Some(token) = maybe_token else { 436 + return Err(MissingToken {}.into()); 437 + }; 438 + 439 + if token == "" { 440 + return Err(MissingToken {}.into()); 441 + } 442 + 443 + if params.get("token").is_some_and(|val| *val == token) { 444 + return Ok(()); 445 + } 446 + 447 + Err(InvalidToken {}.into()) 448 + } 449 + 450 #[axum::debug_handler] 451 async fn handle_create_do( 452 + session: Session, 453 headers: HeaderMap, 454 Query(params): Query<HashMap<String, String>>, 455 ) -> Result<Response<Body>> { 456 + ensure_token(&session, &params).await?; 457 let owner = ensure_authenticated( 458 &headers, 459 #[cfg(debug_assertions)] ··· 520 521 #[axum::debug_handler] 522 async fn handle_delete_do( 523 + session: Session, 524 headers: HeaderMap, 525 Query(params): Query<HashMap<String, String>>, 526 ) -> Result<Response<Body>> { 527 + ensure_token(&session, &params).await?; 528 ensure_authenticated( 529 &headers, 530 #[cfg(debug_assertions)] ··· 609 .expect("Failed to connect to database defined in $DATABASE_URL after 3 retries") 610 }; 611 612 + let session_layer = { 613 + let session_store = MemoryStore::default(); 614 + SessionManagerLayer::new(session_store).with_secure(false) // must be false for go:// support 615 + }; 616 + 617 sqlx::migrate!() 618 .run(&mut connection) 619 .await ··· 658 .route("/_/search", get(handle_search)) 659 .route("/_/{*route}", get(handle_404)) 660 .route("/{*go}", get(handle_base)); 661 + let app = NormalizePathLayer::trim_trailing_slash().layer(router.layer(session_layer)); 662 663 let listener = tokio::net::TcpListener::bind( 664 env::var("BIND_ADDR").unwrap_or_else(|_| "0.0.0.0:3000".to_string()),

History

6 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
fix(m/security): block Cross-Site Request Forgery
5/5 success
expand
expand 0 comments
closed without merging
1 commit
expand
fix(m/security): block Cross-Site Request Forgery
5/5 success
expand
expand 0 comments
1 commit
expand
fix(m/security): block Cross-Site Request Forgery
5/5 success
expand
expand 0 comments
1 commit
expand
fix(m/security): block Cross-Site Request Forgery
5/5 success
expand
expand 0 comments
1 commit
expand
fix(m/security): block Cross-Site Request Forgery
expand 0 comments
1 commit
expand
fix(m/security): block Cross-Site Request Forgery
expand 0 comments