The Wayback Machine - https://web.archive.org/web/20231210211519/https://github.com/github/codeql/pull/15057
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web Cache Deception Vulnerability on Go Frameworks #15057

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

aydinnyunus
Copy link
Contributor

@aydinnyunus aydinnyunus commented Dec 9, 2023

Pull Request: Add Web Cache Deception Query for CodeQL

Overview

This pull request introduces a new CodeQL query to detect potential Web Cache Deception vulnerabilities in web applications. Web Cache Deception is a security vulnerability where attackers trick a server into caching sensitive information, which they can later access. This query aims to identify code patterns that might make an application susceptible to this type of attack.

Changes Introduced

  • New Query Added: WebCacheDeceptionFiber.ql - Detects patterns where web applications might cache sensitive information inadvertently.
  • New Query Added: WebCacheDeceptionGoChi.ql - Detects patterns where web applications might cache sensitive information inadvertently.
  • Experimental Code Samples: Included in the /examples directory, demonstrating both vulnerable (bad) and secure (good) coding practices related to caching.
  • Documentation: Added documentation explaining the query's purpose, usage, and the nature of the vulnerability.

Implementation Details

  • The query looks for server configurations or code patterns where URLs can be manipulated to cache sensitive information.
  • Focuses on common web languages and frameworks where this vulnerability might occur.

Testing and Validation

  • Query tested against a range of synthetic code samples (included in the PR).
  • Validated for false positive rates and performance impact on large codebases.

Future Work

  • Plan to extend support to additional languages and frameworks.
  • Open to community feedback for further refinement and optimization.

References

@aydinnyunus
Copy link
Contributor Author

Probably I should not push the "vendor" directory. If you approve it I can remove it.

@aydinnyunus aydinnyunus changed the title GoFiber: Web Cache Deception Vulnerability Web Cache Deception Vulnerability on Go Frameworks Dec 9, 2023
@owen-mc
Copy link
Contributor

owen-mc commented Dec 9, 2023

Please use depstubber to create just the stubs that are needed and put those in the vendor directory.

@aydinnyunus
Copy link
Contributor Author

Updated @owen-mc

Copy link
Contributor

github-actions bot commented Dec 10, 2023

QHelp previews:

go/ql/src/experimental/CWE-525/WebCacheDeception.qhelp

Unknown query

Web Cache Deception is a security vulnerability where an attacker tricks a web server into caching sensitive information and then accesses that cached data.

This attack exploits certain behaviors in caching mechanisms by requesting URLs that trick the server into thinking that a non-cachable page is cachable. If a user then accesses sensitive information on these pages, it could be cached and later retrieved by the attacker.

Recommendation

To prevent Web Cache Deception attacks, web applications should clearly define cacheable and non-cacheable resources. Implementing strict cache controls and validating requested URLs can mitigate the risk of sensitive data being cached.

Example

Vulnerable code example: A web server is configured to cache all responses ending in '.css'. An attacker requests 'profile.css', and the server processes 'profile', a sensitive page, and caches it.

package bad

import (
	"fmt"
	"html/template"
	"log"
	"net/http"
	"os/exec"
	"strings"
	"sync"
)

var sessionMap = make(map[string]string)

var (
	templateCache = make(map[string]*template.Template)
	mutex         = &sync.Mutex{}
)

type Lists struct {
	Uid       string
	UserName  string
	UserLists []string
	ReadFile  func(filename string) string
}

func parseTemplateFile(templateName string, tmplFile string) (*template.Template, error) {
	mutex.Lock()
	defer mutex.Unlock()

	// Check if the template is already cached
	if cachedTemplate, ok := templateCache[templateName]; ok {
		fmt.Println("cached")
		return cachedTemplate, nil
	}

	// Parse and store the template in the cache
	parsedTemplate, _ := template.ParseFiles(tmplFile)
	fmt.Println("not cached")

	templateCache[templateName] = parsedTemplate
	return parsedTemplate, nil
}

func ShowAdminPageCache(w http.ResponseWriter, r *http.Request) {

	if r.Method == "GET" {
		fmt.Println("cache called")
		sessionMap[r.RequestURI] = "admin"

		// Check if a session value exists
		if _, ok := sessionMap[r.RequestURI]; ok {
			cmd := "mysql -h mysql -u root -prootwolf -e 'select id,name,mail,age,created_at,updated_at from vulnapp.user where name not in (\"" + "admin" + "\");'"

			// mysql -h mysql -u root -prootwolf -e 'select id,name,mail,age,created_at,updated_at from vulnapp.user where name not in ("test");--';echo");'
			fmt.Println(cmd)

			res, err := exec.Command("sh", "-c", cmd).Output()
			if err != nil {
				fmt.Println("err : ", err)
			}

			splitedRes := strings.Split(string(res), "\n")

			p := Lists{Uid: "1", UserName: "admin", UserLists: splitedRes}

			parsedTemplate, _ := parseTemplateFile("page", "./views/admin/userlists.gtpl")
			w.Header().Set("Cache-Control", "no-store, no-cache")
			err = parsedTemplate.Execute(w, p)
		}
	} else {
		http.NotFound(w, nil)
	}

}

func main() {
	fmt.Println("Vulnapp server listening : 1337")

	http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/"))))

	http.HandleFunc("/adminusers/", ShowAdminPageCache)
	err := http.ListenAndServe(":1337", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

Example

Secure code example: The server is configured with strict cache controls and URL validation, preventing caching of dynamic or sensitive pages regardless of their URL pattern.

package good

import (
	"fmt"
	"html/template"
	"log"
	"net/http"
	"os/exec"
	"strings"
	"sync"
)

var sessionMap = make(map[string]string)

var (
	templateCache = make(map[string]*template.Template)
	mutex         = &sync.Mutex{}
)

type Lists struct {
	Uid       string
	UserName  string
	UserLists []string
	ReadFile  func(filename string) string
}

func parseTemplateFile(templateName string, tmplFile string) (*template.Template, error) {
	mutex.Lock()
	defer mutex.Unlock()

	// Check if the template is already cached
	if cachedTemplate, ok := templateCache[templateName]; ok {
		fmt.Println("cached")
		return cachedTemplate, nil
	}

	// Parse and store the template in the cache
	parsedTemplate, _ := template.ParseFiles(tmplFile)
	fmt.Println("not cached")

	templateCache[templateName] = parsedTemplate
	return parsedTemplate, nil
}

func ShowAdminPageCache(w http.ResponseWriter, r *http.Request) {

	if r.Method == "GET" {
		fmt.Println("cache called")
		sessionMap[r.RequestURI] = "admin"

		// Check if a session value exists
		if _, ok := sessionMap[r.RequestURI]; ok {
			cmd := "mysql -h mysql -u root -prootwolf -e 'select id,name,mail,age,created_at,updated_at from vulnapp.user where name not in (\"" + "admin" + "\");'"

			// mysql -h mysql -u root -prootwolf -e 'select id,name,mail,age,created_at,updated_at from vulnapp.user where name not in ("test");--';echo");'
			fmt.Println(cmd)

			res, err := exec.Command("sh", "-c", cmd).Output()
			if err != nil {
				fmt.Println("err : ", err)
			}

			splitedRes := strings.Split(string(res), "\n")

			p := Lists{Uid: "1", UserName: "admin", UserLists: splitedRes}

			parsedTemplate, _ := parseTemplateFile("page", "./views/admin/userlists.gtpl")
			w.Header().Set("Cache-Control", "no-store, no-cache")
			err = parsedTemplate.Execute(w, p)
		}
	} else {
		http.NotFound(w, nil)
	}

}

func main() {
	fmt.Println("Vulnapp server listening : 1337")

	http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/"))))

	http.HandleFunc("/adminusers", ShowAdminPageCache)
	err := http.ListenAndServe(":1337", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

Example

Vulnerable code example: The server is configured with strict cache controls and URL validation, preventing caching of dynamic or sensitive pages regardless of their URL pattern.

package fiber

import (
	"fmt"
	"log"

	"github.com/gofiber/fiber/v2"
)

func main() {
	app := fiber.New()
	log.Println("We are logging in Golang!")

	// GET /api/register
	app.Get("/api/*", func(c *fiber.Ctx) error {
		msg := fmt.Sprintf("✋")
		return c.SendString(msg) // => ✋ register
	})

	app.Post("/api/*", func(c *fiber.Ctx) error {
		msg := fmt.Sprintf("✋")
		return c.SendString(msg) // => ✋ register
	})

	// GET /flights/LAX-SFO
	app.Get("/flights/:from-:to", func(c *fiber.Ctx) error {
		msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to"))
		return c.SendString(msg) // => 💸 From: LAX, To: SFO
	})

	// GET /dictionary.txt
	app.Get("/:file.:ext", func(c *fiber.Ctx) error {
		msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext"))
		return c.SendString(msg) // => 📃 dictionary.txt
	})

	log.Fatal(app.Listen(":3000"))
}

Example

Vulnerable code example: The server is configured with strict cache controls and URL validation, preventing caching of dynamic or sensitive pages regardless of their URL pattern.

package main

import (
	"net/http"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
)

func main() {
	r := chi.NewRouter()
	r.Use(middleware.Logger)
	r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("welcome"))
	})
	http.ListenAndServe(":3000", r)
}

References

from DataFlow::CallNode httpHandleFuncCall, ImportSpec importSpec
where
importSpec.getPath() = "github.com/gofiber/fiber/v2" and
httpHandleFuncCall.getCall().getArgument(0).toString().matches("%/*%") and

Check warning

Code scanning / CodeQL

Using 'toString' in query logic Warning

Query logic depends on implementation of 'toString'.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getStringValue does not works for me, so I use toString

where
importSpec.getPath() = "github.com/gofiber/fiber/v2" and
httpHandleFuncCall.getCall().getArgument(0).toString().matches("%/*%") and
not httpHandleFuncCall.getCall().getArgument(0).toString().matches("%$%") and

Check warning

Code scanning / CodeQL

Using 'toString' in query logic Warning

Query logic depends on implementation of 'toString'.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getStringValue does not works for me, so I use toString

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What string does getStringValue give you? Or does it not have a value in a case that you care about? Queries cannot use toString.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why but I did not get the endpoint which is "/*". I use getType() for getting type but it is not string for CodeQL.

Code Sample:

r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("welcome"))
	})

When I use getStringValue it did not get this argument.

from DataFlow::CallNode httpHandleFuncCall, ImportSpec importSpec
where
importSpec.getPath() = "github.com/go-chi/chi/v5" and
httpHandleFuncCall.getCall().getArgument(0).toString().matches("%/*%") and

Check warning

Code scanning / CodeQL

Using 'toString' in query logic Warning

Query logic depends on implementation of 'toString'.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getStringValue does not works for me, so I use toString

where
importSpec.getPath() = "github.com/go-chi/chi/v5" and
httpHandleFuncCall.getCall().getArgument(0).toString().matches("%/*%") and
not httpHandleFuncCall.getCall().getArgument(0).toString().matches("%$%") and

Check warning

Code scanning / CodeQL

Using 'toString' in query logic Warning

Query logic depends on implementation of 'toString'.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getStringValue does not works for me, so I use toString

@aydinnyunus
Copy link
Contributor Author

fixed the qhelp issue and tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
3 participants