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
base: main
Are you sure you want to change the base?
Conversation
|
Please put the query in |
|
Will you be submitting a bug bounty application for this PR? If so then you can start the process now. |
|
Hi, Yes I submitted to the Bug Bounty Platform.
|
|
QHelp previews: go/ql/src/experimental/CWE-525/WebCacheDeception.qhelpUnknown queryWeb 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. RecommendationTo 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. ExampleVulnerable 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)
}
}ExampleSecure 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
| 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
| // 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
| // 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
There was a problem hiding this 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).
|
|
i tried on latest release but i did not understand the failed checks. |
There was a problem hiding this 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.
|
There was a problem hiding this 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>.
|
Also, you do not need to merge |
|
|
okay thank you for your information |
There was a problem hiding this 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.
| 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
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.
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>
|
Oh, I was wrong about the |
There was a problem hiding this 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.
In |
|
I think what you need to add is
(You should probably define By the way, does it matter what value the "Cache-Control" header is set to? |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * @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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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.
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
WebCacheDeception.ql- Detects patterns where web applications might cache sensitive information inadvertently./examplesdirectory, demonstrating both vulnerable (bad) and secure (good) coding practices related to caching.Implementation Details
Testing and Validation
Future Work
References