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

Golang: Web Cache Deception Vulnerability #14775

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

Conversation

aydinnyunus
Copy link

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: WebCacheDeception.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

@owen-mc
Copy link
Contributor

owen-mc commented Nov 13, 2023

Please put the query in go/ql/src/experimental/CWE-525. You seem to accidentally have two (slightly different?) copies of the query. Please put tests in go/ql/test/experimental/CWE-525 with a .qlref file pointing to the query. No need for a change note since it will be in experimental. (and change notes have to be formatted following this guide.) Please don't commit the database, or anything else in the wcd directory.

@owen-mc
Copy link
Contributor

owen-mc commented Nov 13, 2023

Will you be submitting a bug bounty application for this PR? If so then you can start the process now.

@aydinnyunus
Copy link
Author

Hi,

Yes I submitted to the Bug Bounty Platform.

  • I removed wcd directory
  • Change changelog format
  • Move tests to go/ql/test/experimental/CWE-525
  • Move query to go/ql/src/experimental/CWE-525

Copy link
Contributor

github-actions bot commented Nov 14, 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 main

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 main

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)
	}
}

References

where
httpHandleFuncCall.getTarget().hasQualifiedName("net/http", "HandleFunc") and
httpHandleFuncCall.getNumArgument() > 1 and
httpHandleFuncCall.getArgument(0).getType().toString() = "string" and

Check warning

Code scanning / CodeQL

Using 'toString' in query logic Warning

Query logic depends on implementation of 'toString'.
httpHandleFuncCall.getTarget().hasQualifiedName("net/http", "HandleFunc") and
httpHandleFuncCall.getNumArgument() > 1 and
httpHandleFuncCall.getArgument(0).getType().toString() = "string" and
httpHandleFuncCall.getArgument(0).toString().matches("%/\"") and

Check warning

Code scanning / CodeQL

Using 'toString' in query logic Warning

Query logic depends on implementation of 'toString'.
// Find the corresponding expression for the predecessor
get.hasQualifiedName("net/http", "Header", "Set") and
call = get.getACall() and
call.getArgument(0).toString().matches("\"Cache-Control\"")

Check warning

Code scanning / CodeQL

Using 'toString' in query logic Warning

Query logic depends on implementation of 'toString'.
// Find the corresponding expression for the predecessor
get.hasQualifiedName("net/http", "Header", "Set") and
call = get.getACall() and
call.getArgument(0).toString().matches("\"Cache-Control\"")

Check notice

Code scanning / CodeQL

Use of regexp to match a set of constant string Note

Use string comparison instead of regexp to compare against a constant set of string.
Copy link
Contributor

@owen-mc owen-mc left a comment

Choose a reason for hiding this comment

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

Please add a file go/ql/test/experimental/CWE-525/WebCacheDeception.qlref containing the line experimental/CWE-525/WebCacheDeception.ql. This will make the tests run. Please also delete go/ql/test/experimental/CWE-525/WebCacheDeception.qhelp (we only need one copy, with the query).

@aydinnyunus
Copy link
Author

  • i removed the qhelp file and add qlref file
  • submitted to the codeql bug bounty

@ghsecuritylab ghsecuritylab marked this pull request as draft November 14, 2023 16:02
@aydinnyunus aydinnyunus marked this pull request as ready for review November 14, 2023 16:19
@aydinnyunus
Copy link
Author

i tried on latest release but i did not understand the failed checks.

Copy link
Contributor

@owen-mc owen-mc left a comment

Choose a reason for hiding this comment

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

Do you know how to run the tests locally? There are some basic problems that need to be fixed. The ones that are labelled "CONSISTENCY/UnexpectedFrontendErrors.ql" in the CI results are build failures for the test file. If you run go build ." in the test folder you should see the problems. You also need a WebCacheDeception.expected` file showing the results you expect from running the query on the test file.

Please delete the change note - this is not needed for an experimental query.

@aydinnyunus
Copy link
Author

  • i removed the change notes and fix the build error

Copy link
Contributor

@owen-mc owen-mc left a comment

Choose a reason for hiding this comment

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

There are still some failures compiling the test files. I'm not sure if you can see the logs. The main problem is that the same global variables are declared in two files in the same directory. One way to avoid this would be to combine the two files in the test folder. Another would be to give the variables different names, e.g. sessionMap1 and sessionMap2. Also portNum is declared but not used.

You also need to create and commit a .expected file. You can generate this by running the tests with codeql test run --learn <path to folder>.

@owen-mc
Copy link
Contributor

owen-mc commented Nov 14, 2023

Also, you do not need to merge main into the branch. Do not worry about the warning that this branch is out-of-date with the base branch. Because github/codeql is a monorepo there are lots of PRs being merged that do not have any effect on this PR. If there is a conflict then there will be a different warning and some of the CI won't run, so it will be clear that it needs to be fixed.

@aydinnyunus
Copy link
Author

  • i added expected file
  • fixed test errors

@aydinnyunus
Copy link
Author

okay thank you for your information

Copy link
Contributor

@owen-mc owen-mc left a comment

Choose a reason for hiding this comment

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

I think the .expected file is malformed. It's also showing results in the good example as well as the bad one.

go/ql/src/experimental/CWE-525/WebCacheDeception.ql Outdated Show resolved Hide resolved
go/ql/src/experimental/CWE-525/WebCacheDeception.ql Outdated Show resolved Hide resolved
go/ql/src/experimental/CWE-525/WebCacheDeception.ql Outdated Show resolved Hide resolved
go/ql/src/experimental/CWE-525/WebCacheDeception.ql Outdated Show resolved Hide resolved
go/ql/src/experimental/CWE-525/WebCacheDeception.ql Outdated Show resolved Hide resolved
get.hasQualifiedName("net/http", "Header", "Set") and
call = get.getACall() and
call.getArgument(0).toString().matches("\"Cache-Control\"")
select httpHandleFuncCall.getArgument(0), call.getArgument(0)
Copy link
Contributor

@owen-mc owen-mc Nov 15, 2023

Choose a reason for hiding this comment

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

This is not the right format for a path query. I will try to find some documentation to show you what it should be.

Copy link
Contributor

@owen-mc owen-mc Nov 15, 2023

Choose a reason for hiding this comment

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

https://codeql.github.com/docs/writing-codeql-queries/creating-path-queries/#constructing-a-path-query
But actually, this isn't a path query, as it's currently written.

Copy link
Author

Choose a reason for hiding this comment

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

Yes it is checking for response Header. Is it okay now ?

get.hasQualifiedName("net/http", "Header", "Set") and
call = get.getACall() and
call.getArgument(0).getStringValue() = "Cache-Control"

Copy link
Contributor

Choose a reason for hiding this comment

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

Currently this query checks that (1) httpHandleFuncCall is a call to HandleFunc and its first argument ends in "/", and (2) call is a call to Header.Set and its first argument is "Cache-Control". What it doesn't do it relate those two things in any way. These two calls could be in totally different files, and not relate to the same objects. I'm not totally sure how you want to relate them. Perhaps the next paragraph will help.

I looked for where Header.Set is already used in our code and found private class HeaderWriteCall in go/ql/lib/semmle/go/frameworks/stdlib/NetHttp.qll. It's private so you can't use it directly. But it extends Http::HeaderWrite::Range, so you can use Http::HeaderWrite and get it (and many more classes which do similar things in other libraries). It is defined here. If you aren't familiar with "the range pattern" this might be bit confusing, but basically try using HeaderWrite and it should just work. Do you think you could HeaderWrite instead?

Copy link
Contributor

Choose a reason for hiding this comment

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

To be clear, I'm expecting something like "there exists a HeaderWrite with getHeaderName() = A and getHeaderValue = B" plus something linking the headerwrite to httpHandleFuncCall.

Copy link
Author

Choose a reason for hiding this comment

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

I use HeaderWrite::Range in following syntax.

hw.getHeaderName() = "cache-control"

Copy link
Author

Choose a reason for hiding this comment

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

Yes. In my case I am checking is Cache Control used in the following function which used in the endpoint but I don't think that this HeaderWrite::Range check the following function. I think first method decrease the False Positive percentage.

aydinnyunus and others added 6 commits November 15, 2023 15:08
Co-authored-by: Owen Mansel-Chan <62447351+owen-mc@users.noreply.github.com>
Co-authored-by: Owen Mansel-Chan <62447351+owen-mc@users.noreply.github.com>
Co-authored-by: Owen Mansel-Chan <62447351+owen-mc@users.noreply.github.com>
@owen-mc
Copy link
Contributor

owen-mc commented Nov 15, 2023

Oh, I was wrong about the .expected file being bad. It was just wrapping weirdly on my screen.

Copy link
Contributor

@owen-mc owen-mc left a comment

Choose a reason for hiding this comment

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

Can you tell me in words how the header write should be linked to the call to HandleFunc? In fact, it might be helpful to describe exactly what you want the whole query to do in words, so that I can help you get it doing the right thing.

@aydinnyunus
Copy link
Author

Can you tell me in words how the header write should be linked to the call to HandleFunc?

In HandleFunc, if first argument contains "/" at the end. It means "/admin/asd.css" and "/admin" shows the same page. I want to check in second parameter in HandleFunc, if caching mechanism works on that endpoint it can cause Web Cache Deception attacks. So I checked the provided function in second parameter. Is the function have "Cache-Control". But In some cases it has no necessary to do it on that function. So I use HeaderWrite::Range but it maybe cause false positive issues if that endpoint not cached.

@owen-mc
Copy link
Contributor

owen-mc commented Nov 16, 2023

I think what you need to add is

  • the second argument of httpHandleFuncCall is a DataFlow::ReadNode called rn
  • there is a DeclaredFunction called f which satisfies rn.reads(f)
  • the first parameter of f is hw.getResponseWriter()

(You should probably define rn and f in the from clause of the query.)

By the way, does it matter what value the "Cache-Control" header is set to?

@aydinnyunus
Copy link
Author

aydinnyunus commented Nov 16, 2023

I think what you need to add is

  • the second argument of httpHandleFuncCall is a DataFlow::ReadNode called rn
  • there is a DeclaredFunction called f which satisfies rn.reads(f)
  • the first parameter of f is hw.getResponseWriter()

(You should probably define rn and f in the from clause of the query.)

By the way, does it matter what value the "Cache-Control" header is set to?

from DataFlow::CallNode httpHandleFuncCall, DataFlow::ReadNode rn, Http::HeaderWrite::Range hw, DeclaredFunction f
where
  httpHandleFuncCall.getTarget().hasQualifiedName("net/http", "HandleFunc") and
  httpHandleFuncCall.getArgument(0).getType().getUnderlyingType() instanceof StringType and
  httpHandleFuncCall.getArgument(0).getStringValue().matches("%/") and
  rn.reads(f) and
  f.getParameter(0) = hw.getResponseWriter() and
  hw.getHeaderName() = "cache-control"
select httpHandleFuncCall.getArgument(0), hw.getResponseWriter()

sorry for previous comment. header value is not necessary in my case

DeclaredFunction f
where
httpHandleFuncCall.getTarget().hasQualifiedName("net/http", "HandleFunc") and
httpHandleFuncCall.getArgument(0).getType().getUnderlyingType() instanceof StringType and
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
httpHandleFuncCall.getArgument(0).getType().getUnderlyingType() instanceof StringType and

This line is not needed because it is implied by the line below.

httpHandleFuncCall.getTarget().hasQualifiedName("net/http", "HandleFunc") and
httpHandleFuncCall.getArgument(0).getType().getUnderlyingType() instanceof StringType and
httpHandleFuncCall.getArgument(0).getStringValue().matches("%/") and
rn.reads(f) and
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
rn.reads(f) and
httpHandleFuncCall.getArgument(1) = rn and
rn.reads(f) and

/*
* @name Web Cache Deception
* @description A caching system has been detected on the application and is vulnerable to web cache deception. By manipulating the URL it is possible to force the application to cache pages that are only accessible by an authenticated user. Once cached, these pages can be accessed by an unauthenticated user.
* @kind path-problem
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @kind path-problem
* @kind problem

rn.reads(f) and
f.getParameter(0) = hw.getResponseWriter() and
hw.getHeaderName() = "cache-control"
select httpHandleFuncCall.getArgument(0), hw.getResponseWriter()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
select httpHandleFuncCall.getArgument(0), hw.getResponseWriter()
select httpHandleFuncCall.getArgument(0), hw.getResponseWriter()

Please follow these instructions on the format of the select clause. In particular, there needs to be a string explaining briefly what the problem is. If you want examples. search the repo for "@kind problem" to find other non-path queries and look at what message they have in their select clause.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants