diff --git a/solution_28.md b/solution_28.md
new file mode 100644
index 0000000..de7ba8d
--- /dev/null
+++ b/solution_28.md
@@ -0,0 +1,139 @@
+# Solution for #28: fix: reject path traversal in the post id route
+
+===FILE:src/main.rs===
+#[macro_use] extern crate rocket;
+
+use rocket::fs::NamedFile;
+use rocket::response::content::RawHtml;
+use rocket::serde::json::Json;
+use rocket::State;
+use std::path::PathBuf;
+use std::fs;
+use std::io;
+
+// -----------------------------------------------------------------------------
+// Post ID validation – fixes CWE-22 path traversal (GitHub #27)
+// Accepts only slugs that the application generates or expects:
+// [A-Za-z0-9_-] with a max length of 128.
+// -----------------------------------------------------------------------------
+fn is_valid_post_id(id: &str) -> bool {
+ !id.is_empty()
+ && id.len() <= 128
+ && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
+}
+
+// -----------------------------------------------------------------------------
+// Routes
+// -----------------------------------------------------------------------------
+
+// The home page (optional, just for completeness)
+#[get("/")]
+async fn index() -> RawHtml<&'static str> {
+ RawHtml("
nonogra.ph
Welcome!
")
+}
+
+// Main post view – rejects invalid post IDs before touching the filesystem
+#[get("/")]
+async fn view_post(post_id: String) -> Result, rocket::response::status::NotFound> {
+ if !is_valid_post_id(&post_id) {
+ return Err(rocket::response::status::NotFound("Invalid post ID".to_string()));
+ }
+
+ let path = PathBuf::from("content").join(format!("{}.md", post_id));
+ match fs::read_to_string(&path) {
+ Ok(content) => {
+ // Simple markdown rendering (replace with your actual renderer)
+ let html = format!("{}", content);
+ Ok(RawHtml(html))
+ }
+ Err(_) => Err(rocket::response::status::NotFound("Post not found".to_string())),
+ }
+}
+
+// No‑JavaScript fallback – delegates to the same validation
+#[get("/nojs/")]
+async fn view_post_nojs(post_id: String) -> Result, rocket::response::status::NotFound> {
+ view_post(post_id).await
+}
+
+// Serve static assets (if any)
+#[get("/static/")]
+async fn static_files(file: PathBuf) -> Option {
+ NamedFile::open(PathBuf::from("static").join(file)).await.ok()
+}
+
+// -----------------------------------------------------------------------------
+// Rocket launch
+// -----------------------------------------------------------------------------
+#[launch]
+fn rocket() -> _ {
+ rocket::build()
+ .mount("/", routes![index, view_post, view_post_nojs, static_files])
+}
+
+// -----------------------------------------------------------------------------
+// Unit tests
+// -----------------------------------------------------------------------------
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_valid_slugs() {
+ // Generated posts (generate_post_id emits [a-z0-9-])
+ assert!(is_valid_post_id("hello"));
+ assert!(is_valid_post_id("hello-world"));
+ assert!(is_valid_post_id("abc-123"));
+ // Static pages (lowercase words)
+ assert!(is_valid_post_id("about"));
+ assert!(is_valid_post_id("contact"));
+ // Telegraph archiver output ([A-Za-z0-9_-])
+ assert!(is_valid_post_id("Telegraph_Archiver-123"));
+ assert!(is_valid_post_id("foo_bar"));
+ assert!(is_valid_post_id("ABC123"));
+ // Edge cases
+ assert!(is_valid_post_id("a"));
+ assert!(is_valid_post_id("z".repeat(128).as_str()));
+ }
+
+ #[test]
+ fn test_rejected_traversal_vectors() {
+ assert!(!is_valid_post_id(".."));
+ assert!(!is_valid_post_id("../README"));
+ assert!(!is_valid_post_id("..\\README"));
+ assert!(!is_valid_post_id("foo/bar"));
+ assert!(!is_valid_post_id("foo.bar"));
+ assert!(!is_valid_post_id("post.md"));
+ assert!(!is_valid_post_id("foo\0bar")); // embedded NUL
+ assert!(!is_valid_post_id("")); // empty
+ assert!(!is_valid_post_id("."));
+ assert!(!is_valid_post_id("..."));
+ assert!(!is_valid_post_id("foo/../bar"));
+ }
+
+ #[test]
+ fn test_length_bound() {
+ let long = "a".repeat(129);
+ assert!(!is_valid_post_id(&long));
+ let short = "a".repeat(128);
+ assert!(is_valid_post_id(&short));
+ }
+
+ #[test]
+ fn test_generate_post_id_invariant() {
+ // Any ID that generate_post_id can produce must pass validation.
+ // Since we don't have that function here, we assert that all
+ // characters from its alphabet are accepted.
+ let alphabet = "abcdefghijklmnopqrstuvwxyz0123456789-";
+ for c in alphabet.chars() {
+ let id = c.to_string();
+ assert!(is_valid_post_id(&id));
+ }
+ // Mixed cases and underscore (if generate_post_id ever uses them)
+ assert!(is_valid_post_id("Test_123"));
+ }
+}
+===END_FILE===
+
+---
+_Generated by DevilX BountyHub solver_