diff --git a/go/src/security/DeferReleaseInLoop/DeferReleaseInLoop.go b/go/src/security/DeferReleaseInLoop/DeferReleaseInLoop.go new file mode 100644 index 0000000..68b32ac --- /dev/null +++ b/go/src/security/DeferReleaseInLoop/DeferReleaseInLoop.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "os" +) + +// BAD: defer Close inside a loop leaks file descriptors +func badReadFiles(paths []string) { + for _, path := range paths { + f, err := os.Open(path) + if err != nil { + continue + } + defer f.Close() // not closed until function returns + fmt.Println(f.Name()) + } +} + +// GOOD: extract into a function so defer runs per iteration +func goodReadFiles(paths []string) { + for _, path := range paths { + func() { + f, err := os.Open(path) + if err != nil { + return + } + defer f.Close() // closed at end of this closure + fmt.Println(f.Name()) + }() + } +} diff --git a/go/src/security/DeferReleaseInLoop/DeferReleaseInLoop.qhelp b/go/src/security/DeferReleaseInLoop/DeferReleaseInLoop.qhelp new file mode 100644 index 0000000..fd24c91 --- /dev/null +++ b/go/src/security/DeferReleaseInLoop/DeferReleaseInLoop.qhelp @@ -0,0 +1,42 @@ + + + +

+In Go, defer schedules a function call to run when the enclosing function +returns — not when the enclosing block or loop iteration ends. Deferring a resource release +call (such as Close, Unlock, or Rollback) inside a loop +means that cleanup calls accumulate and only execute after the loop finishes and the function +returns. +

+ +

+This can lead to resource exhaustion: file descriptors pile up, database connections are held +open, locks are held longer than intended, or transactions remain open across iterations. +

+ +
+ +

+Extract the loop body into a separate function or closure so that defer runs +at the end of each iteration: +

+ + + +

+Alternatively, call the cleanup function directly without defer at the +appropriate point in the loop body. +

+ +
+ +
  • + Go Language Specification — Defer statements +
  • +
  • + Gotchas of Defer in Go +
  • +
    +
    diff --git a/go/src/security/DeferReleaseInLoop/DeferReleaseInLoop.ql b/go/src/security/DeferReleaseInLoop/DeferReleaseInLoop.ql new file mode 100644 index 0000000..d27e22b --- /dev/null +++ b/go/src/security/DeferReleaseInLoop/DeferReleaseInLoop.ql @@ -0,0 +1,93 @@ +/** + * @name Deferred resource release in loop + * @id tob/go/defer-release-in-loop + * @description Deferring a resource release (Close, Rollback, etc.) inside a loop delays cleanup until the enclosing function returns, causing resource leaks across iterations such as file descriptor exhaustion or connection pool starvation. + * @kind problem + * @tags security + * @problem.severity warning + * @precision medium + * @security-severity 3.0 + * @group security + */ + +import go + +/** + * A function that acquires a resource that could leak + */ +class ResourceAcquisition extends Function { + ResourceAcquisition() { + this.hasQualifiedName("os", ["Open", "OpenFile", "Create", "CreateTemp", "NewFile", "Pipe"]) + or + this.hasQualifiedName("net", ["Dial", "DialTimeout", "Listen", "ListenPacket"]) + or + this.hasQualifiedName("net", ["FileConn", "FileListener", "FilePacketConn"]) + or + this.(Method).hasQualifiedName("net", "Dialer", ["Dial", "DialContext"]) + or + this.hasQualifiedName("net/http", ["Get", "Post", "PostForm", "Head"]) + or + this.(Method).hasQualifiedName("net/http", "Client", ["Do", "Get", "Post", "PostForm", "Head"]) + or + this.hasQualifiedName("crypto/tls", ["Dial", "DialWithDialer", "Client", "Server"]) + or + this.(Method).hasQualifiedName("crypto/tls", "Dialer", "DialContext") + or + this.hasQualifiedName("compress/gzip", ["NewReader", "NewWriter", "NewWriterLevel"]) + or + this.hasQualifiedName("compress/zlib", + ["NewReader", "NewWriter", "NewWriterLevel", "NewWriterLevelDict"]) + or + this.hasQualifiedName("compress/flate", ["NewReader", "NewWriter"]) + or + this.hasQualifiedName("compress/lzw", ["NewReader", "NewWriter"]) + or + this.hasQualifiedName("archive/zip", "OpenReader") + } +} + +/** + * Holds if `inner` is a (transitive) child of `outer` without crossing + * a function literal boundary. + */ +predicate parentWithoutFuncLit(AstNode inner, AstNode outer) { + inner.getParent() = outer and not inner instanceof FuncLit + or + exists(AstNode mid | + parentWithoutFuncLit(inner, mid) and + parentWithoutFuncLit(mid, outer) + ) +} + +/** Holds if `node` is inside the body of `loop`, not crossing closures. */ +predicate inLoopBody(AstNode node, LoopStmt loop) { + parentWithoutFuncLit(node, loop.(ForStmt).getBody()) + or + parentWithoutFuncLit(node, loop.(RangeStmt).getBody()) +} + +/** + * Gets the innermost identifier used as the receiver in `defer x.Close()`. + * For `defer f.Close()`, this is `f`. + * For `defer resp.Body.Close()`, this is `resp`. + */ +DataFlow::Node deferCloseReceiverBase(DeferStmt d) { + d.getCall().getTarget().getName() = "Close" and + exists(Expr base | base = d.getCall().getCalleeExpr().(SelectorExpr).getBase() | + // defer f.Close() — base is an identifier + result.asExpr() = base.(Ident) + or + // defer resp.Body.Close() — base is a selector, take its base identifier + result.asExpr() = base.(SelectorExpr).getBase().(Ident) + ) +} + +from DeferStmt deferStmt, DataFlow::CallNode acquisition, LoopStmt loop +where + acquisition.getTarget() instanceof ResourceAcquisition and + inLoopBody(acquisition.asExpr(), loop) and + inLoopBody(deferStmt, loop) and + DataFlow::localFlow(acquisition.getResult(0), deferCloseReceiverBase(deferStmt)) +select deferStmt, + "Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations.", + acquisition, acquisition.getTarget().getName() + "()" diff --git a/go/src/security/UnboundedIORead/UnboundedIORead.go b/go/src/security/UnboundedIORead/UnboundedIORead.go new file mode 100644 index 0000000..aeccad8 --- /dev/null +++ b/go/src/security/UnboundedIORead/UnboundedIORead.go @@ -0,0 +1,19 @@ +package main + +import ( + "io" + "net/http" +) + +// BAD: unbounded read of request body +func badHandler(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) // no size limit — OOM on large request + w.Write(body) +} + +// GOOD: limit body size before reading +func goodHandler(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB limit + body, _ := io.ReadAll(r.Body) + w.Write(body) +} diff --git a/go/src/security/UnboundedIORead/UnboundedIORead.qhelp b/go/src/security/UnboundedIORead/UnboundedIORead.qhelp new file mode 100644 index 0000000..4278e7b --- /dev/null +++ b/go/src/security/UnboundedIORead/UnboundedIORead.qhelp @@ -0,0 +1,35 @@ + + + +

    +Reading an HTTP request body with io.ReadAll (or the deprecated +ioutil.ReadAll) allocates the entire body into memory with no upper bound. +A malicious client can send an arbitrarily large request body to exhaust server memory, +causing a denial-of-service condition. +

    + +
    + +

    +Wrap the request body with a size-limiting reader before reading it: +

    + + + +

    +Prefer http.MaxBytesReader which also sets the appropriate error on the +response, or io.LimitReader for non-HTTP contexts. +

    + +
    + +
  • + http.MaxBytesReader documentation +
  • +
  • + io.LimitReader documentation +
  • +
    +
    diff --git a/go/src/security/UnboundedIORead/UnboundedIORead.ql b/go/src/security/UnboundedIORead/UnboundedIORead.ql new file mode 100644 index 0000000..c15047f --- /dev/null +++ b/go/src/security/UnboundedIORead/UnboundedIORead.ql @@ -0,0 +1,101 @@ +/** + * @name Unbounded read of request body + * @id tob/go/unbounded-io-read + * @description Reading an HTTP request body with `io.ReadAll` or `ioutil.ReadAll` without a size limit allows a malicious client to exhaust server memory with an arbitrarily large request. + * @kind path-problem + * @tags security + * @problem.severity error + * @precision high + * @security-severity 7.5 + * @group security + */ + +import go + +/** + * An `io.ReadAll` or `ioutil.ReadAll` call — functions that read an entire + * reader into memory with no size bound. + */ +class UnboundedReadCall extends DataFlow::CallNode { + UnboundedReadCall() { + this.getTarget().hasQualifiedName("io", "ReadAll") + or + this.getTarget().hasQualifiedName("io/ioutil", "ReadAll") + or + this.getTarget().hasQualifiedName("ioutil", "ReadAll") + } +} + +/** + * A source node: an HTTP request body that can be controlled by an attacker. + * + * Matches `r.Body` where `r` is an `*http.Request`. + */ +class HTTPRequestBodySource extends DataFlow::Node { + HTTPRequestBodySource() { + exists(Field f, SelectorExpr sel | + f.hasQualifiedName("net/http", "Request", "Body") and + sel.getSelector().refersTo(f) and + this.asExpr() = sel + ) + } +} + +/** + * A call that wraps a reader with a size limit, acting as a sanitizer. + */ +class SizeLimitingCall extends DataFlow::CallNode { + SizeLimitingCall() { + this.getTarget().hasQualifiedName("net/http", "MaxBytesReader") + or + this.getTarget().hasQualifiedName("io", "LimitReader") + } +} + +/** + * Holds if `r.Body` is reassigned from a size-limiting call in the same function + * that `bodyRead` is in, on the same request variable `r`. + */ +predicate bodyLimitedByReassignment(SelectorExpr bodyRead) { + exists(SizeLimitingCall limiter, Assignment assign, SelectorExpr lhs, Variable v | + // The assignment target is `v.Body` + assign.getLhs() = lhs and + lhs.getSelector().getName() = "Body" and + lhs.getBase().(Ident).refersTo(v) and + // The RHS is a MaxBytesReader/LimitReader call + assign.getRhs() = limiter.asExpr() and + // The body read is on the same variable: `v.Body` + bodyRead.getBase().(Ident).refersTo(v) and + // Both in the same function + assign.getEnclosingFunction() = bodyRead.getEnclosingFunction() + ) +} + +module UnboundedReadConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { + source instanceof HTTPRequestBodySource and + // Exclude r.Body reads where r.Body was reassigned from a size limiter + not bodyLimitedByReassignment(source.asExpr()) + } + + predicate isSink(DataFlow::Node sink) { + exists(UnboundedReadCall readAll | sink = readAll.getArgument(0)) + } + + predicate isBarrier(DataFlow::Node node) { + // If the body passes through a size-limiting wrapper (io.LimitReader pattern), it is safe + node = any(SizeLimitingCall c).getResult(0) + or + node = any(SizeLimitingCall c).getResult() + } +} + +module UnboundedReadFlow = DataFlow::Global; + +import UnboundedReadFlow::PathGraph + +from UnboundedReadFlow::PathNode source, UnboundedReadFlow::PathNode sink +where UnboundedReadFlow::flowPath(source, sink) +select sink.getNode(), source, sink, + "Unbounded read of HTTP request $@ with no size limit allows denial of service via memory exhaustion.", + source.getNode(), "body" diff --git a/go/test/query-tests/security/DeferReleaseInLoop/DeferReleaseInLoop.expected b/go/test/query-tests/security/DeferReleaseInLoop/DeferReleaseInLoop.expected new file mode 100644 index 0000000..9eeaec9 --- /dev/null +++ b/go/test/query-tests/security/DeferReleaseInLoop/DeferReleaseInLoop.expected @@ -0,0 +1,14 @@ +| DeferReleaseInLoop.go:22:3:22:17 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:18:13:18:25 | call to Open | Open() | +| DeferReleaseInLoop.go:34:3:34:17 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:30:13:30:52 | call to Create | Create() | +| DeferReleaseInLoop.go:46:3:46:17 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:42:13:42:45 | call to OpenFile | OpenFile() | +| DeferReleaseInLoop.go:58:3:58:17 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:54:13:54:39 | call to CreateTemp | CreateTemp() | +| DeferReleaseInLoop.go:70:3:70:25 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:66:16:66:28 | call to Get | Get() | +| DeferReleaseInLoop.go:83:3:83:25 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:79:16:79:29 | call to Do | Do() | +| DeferReleaseInLoop.go:95:3:95:20 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:91:16:91:36 | call to Dial | Dial() | +| DeferReleaseInLoop.go:107:3:107:18 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:103:14:103:40 | call to Listen | Listen() | +| DeferReleaseInLoop.go:124:3:124:18 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:119:14:119:30 | call to NewReader | NewReader() | +| DeferReleaseInLoop.go:125:3:125:17 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:115:13:115:25 | call to Open | Open() | +| DeferReleaseInLoop.go:142:3:142:18 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:137:14:137:30 | call to NewReader | NewReader() | +| DeferReleaseInLoop.go:143:3:143:17 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:133:13:133:25 | call to Open | Open() | +| DeferReleaseInLoop.go:155:3:155:18 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:151:14:151:33 | call to OpenReader | OpenReader() | +| DeferReleaseInLoop.go:168:4:168:18 | defer statement | Deferred Close() of resource acquired from $@ in a loop will not execute until the function returns, leaking resources across iterations. | DeferReleaseInLoop.go:164:14:164:26 | call to Open | Open() | diff --git a/go/test/query-tests/security/DeferReleaseInLoop/DeferReleaseInLoop.go b/go/test/query-tests/security/DeferReleaseInLoop/DeferReleaseInLoop.go new file mode 100644 index 0000000..0933695 --- /dev/null +++ b/go/test/query-tests/security/DeferReleaseInLoop/DeferReleaseInLoop.go @@ -0,0 +1,258 @@ +package main + +import ( + "archive/zip" + "compress/gzip" + "compress/zlib" + "fmt" + "net" + "net/http" + "os" + "strings" + "sync" +) + +// finding: os.Open in range loop +func test_os_open(paths []string) { + for _, path := range paths { + f, err := os.Open(path) + if err != nil { + continue + } + defer f.Close() + fmt.Println(f.Name()) + } +} + +// finding: os.Create in for loop +func test_os_create() { + for i := 0; i < 100; i++ { + f, err := os.Create(fmt.Sprintf("/tmp/file%d", i)) + if err != nil { + continue + } + defer f.Close() + fmt.Println(f.Name()) + } +} + +// finding: os.OpenFile +func test_os_openfile(paths []string) { + for _, path := range paths { + f, err := os.OpenFile(path, os.O_RDONLY, 0) + if err != nil { + continue + } + defer f.Close() + _ = f + } +} + +// finding: os.CreateTemp +func test_os_createtemp(n int) { + for i := 0; i < n; i++ { + f, err := os.CreateTemp("", "prefix") + if err != nil { + continue + } + defer f.Close() + _ = f + } +} + +// finding: http.Get — resp.Body.Close() +func test_http_get(urls []string) { + for _, url := range urls { + resp, err := http.Get(url) + if err != nil { + continue + } + defer resp.Body.Close() + fmt.Println(resp.Status) + } +} + +// finding: http.Client.Do — resp.Body.Close() +func test_http_client_do(client *http.Client, urls []string) { + for _, url := range urls { + req, _ := http.NewRequest("GET", url, nil) + resp, err := client.Do(req) + if err != nil { + continue + } + defer resp.Body.Close() + fmt.Println(resp.Status) + } +} + +// finding: net.Dial +func test_net_dial(addrs []string) { + for _, addr := range addrs { + conn, err := net.Dial("tcp", addr) + if err != nil { + continue + } + defer conn.Close() + fmt.Fprintln(conn, "hello") + } +} + +// finding: net.Listen +func test_net_listen(ports []string) { + for _, port := range ports { + ln, err := net.Listen("tcp", ":"+port) + if err != nil { + continue + } + defer ln.Close() + _ = ln + } +} + +// finding: gzip.NewReader +func test_gzip_reader(paths []string) { + for _, path := range paths { + f, err := os.Open(path) + if err != nil { + continue + } + gz, err := gzip.NewReader(f) + if err != nil { + f.Close() + continue + } + defer gz.Close() + defer f.Close() + _ = gz + } +} + +// finding: zlib.NewReader +func test_zlib_reader(paths []string) { + for _, path := range paths { + f, err := os.Open(path) + if err != nil { + continue + } + zr, err := zlib.NewReader(f) + if err != nil { + f.Close() + continue + } + defer zr.Close() + defer f.Close() + _ = zr + } +} + +// finding: zip.OpenReader +func test_zip_openreader(paths []string) { + for _, path := range paths { + zr, err := zip.OpenReader(path) + if err != nil { + continue + } + defer zr.Close() + _ = zr + } +} + +// finding: nested loop — inner os.Open +func test_nested_loop(paths []string) { + for i := 0; i < 3; i++ { + for _, path := range paths { + f, err := os.Open(path) + if err != nil { + continue + } + defer f.Close() + fmt.Println(f.Name()) + } + } +} + +/* + * False positives that should NOT be flagged + */ + +// ok: defer Close in closure inside loop — runs per iteration +func test_fp_closure(paths []string) { + for _, path := range paths { + func() { + f, err := os.Open(path) + if err != nil { + return + } + defer f.Close() + fmt.Println(f.Name()) + }() + } +} + +// ok: defer Close outside any loop +func test_fp_no_loop() { + f, err := os.Open("/tmp/test") + if err != nil { + return + } + defer f.Close() + fmt.Println(f.Name()) +} + +// ok: non-resource defer in loop +func test_fp_arbitrary_defer(items []string) { + for _, item := range items { + defer fmt.Println(item) + } +} + +// ok: sync.Mutex — not a file/IO resource +func test_fp_mutex(items []string) { + var mu sync.Mutex + for _, item := range items { + mu.Lock() + defer mu.Unlock() + fmt.Println(item) + } +} + +// ok: Close on a resource NOT from FileResourceAcquisition +func test_fp_strings_reader() { + for i := 0; i < 10; i++ { + r := strings.NewReader("hello") + _ = r // strings.Reader has no Close, but even if it did, not modeled + } +} + +// ok: resource acquired outside loop, defer inside is still scoped to function +func test_fp_acquired_outside_loop(path string) { + f, err := os.Open(path) + if err != nil { + return + } + defer f.Close() // not in a loop + for i := 0; i < 10; i++ { + fmt.Fprintln(f, i) + } +} + +func main() { + test_os_open(os.Args[1:]) + test_os_create() + test_os_openfile(os.Args[1:]) + test_os_createtemp(3) + test_http_get(os.Args[1:]) + test_http_client_do(http.DefaultClient, os.Args[1:]) + test_net_dial(os.Args[1:]) + test_net_listen(os.Args[1:]) + test_gzip_reader(os.Args[1:]) + test_zlib_reader(os.Args[1:]) + test_zip_openreader(os.Args[1:]) + test_nested_loop(os.Args[1:]) + test_fp_closure(os.Args[1:]) + test_fp_no_loop() + test_fp_arbitrary_defer(os.Args[1:]) + test_fp_mutex(os.Args[1:]) + test_fp_strings_reader() + test_fp_acquired_outside_loop("/tmp/test") +} diff --git a/go/test/query-tests/security/DeferReleaseInLoop/DeferReleaseInLoop.qlref b/go/test/query-tests/security/DeferReleaseInLoop/DeferReleaseInLoop.qlref new file mode 100644 index 0000000..4dab5be --- /dev/null +++ b/go/test/query-tests/security/DeferReleaseInLoop/DeferReleaseInLoop.qlref @@ -0,0 +1 @@ +security/DeferReleaseInLoop/DeferReleaseInLoop.ql diff --git a/go/test/query-tests/security/DeferReleaseInLoop/go.mod b/go/test/query-tests/security/DeferReleaseInLoop/go.mod new file mode 100644 index 0000000..ce16a2c --- /dev/null +++ b/go/test/query-tests/security/DeferReleaseInLoop/go.mod @@ -0,0 +1,3 @@ +module codeql-go-tests/query/DeferReleaseInLoop + +go 1.18 diff --git a/go/test/query-tests/security/UnboundedIORead/UnboundedIORead.expected b/go/test/query-tests/security/UnboundedIORead/UnboundedIORead.expected new file mode 100644 index 0000000..27a0ed4 --- /dev/null +++ b/go/test/query-tests/security/UnboundedIORead/UnboundedIORead.expected @@ -0,0 +1,12 @@ +edges +| UnboundedIORead.go:24:12:24:17 | selection of Body | UnboundedIORead.go:25:24:25:29 | reader | provenance | Src:MaD:1919 | +nodes +| UnboundedIORead.go:12:24:12:29 | selection of Body | semmle.label | selection of Body | +| UnboundedIORead.go:18:28:18:33 | selection of Body | semmle.label | selection of Body | +| UnboundedIORead.go:24:12:24:17 | selection of Body | semmle.label | selection of Body | +| UnboundedIORead.go:25:24:25:29 | reader | semmle.label | reader | +subpaths +#select +| UnboundedIORead.go:12:24:12:29 | selection of Body | UnboundedIORead.go:12:24:12:29 | selection of Body | UnboundedIORead.go:12:24:12:29 | selection of Body | Unbounded read of HTTP request $@ with no size limit allows denial of service via memory exhaustion. | UnboundedIORead.go:12:24:12:29 | selection of Body | body | +| UnboundedIORead.go:18:28:18:33 | selection of Body | UnboundedIORead.go:18:28:18:33 | selection of Body | UnboundedIORead.go:18:28:18:33 | selection of Body | Unbounded read of HTTP request $@ with no size limit allows denial of service via memory exhaustion. | UnboundedIORead.go:18:28:18:33 | selection of Body | body | +| UnboundedIORead.go:25:24:25:29 | reader | UnboundedIORead.go:24:12:24:17 | selection of Body | UnboundedIORead.go:25:24:25:29 | reader | Unbounded read of HTTP request $@ with no size limit allows denial of service via memory exhaustion. | UnboundedIORead.go:24:12:24:17 | selection of Body | body | diff --git a/go/test/query-tests/security/UnboundedIORead/UnboundedIORead.go b/go/test/query-tests/security/UnboundedIORead/UnboundedIORead.go new file mode 100644 index 0000000..83450cd --- /dev/null +++ b/go/test/query-tests/security/UnboundedIORead/UnboundedIORead.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" +) + +// finding: io.ReadAll on raw request body +func test_readall_raw(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + w.Write(body) +} + +// finding: ioutil.ReadAll on raw request body (deprecated but common) +func test_ioutil_readall_raw(w http.ResponseWriter, r *http.Request) { + body, _ := ioutil.ReadAll(r.Body) + w.Write(body) +} + +// finding: body passed through variable +func test_readall_via_var(w http.ResponseWriter, r *http.Request) { + reader := r.Body + body, _ := io.ReadAll(reader) + w.Write(body) +} + +/* + * False positives that should NOT be flagged + */ + +// ok: body wrapped with MaxBytesReader +func test_fp_maxbytes(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) + body, _ := io.ReadAll(r.Body) + w.Write(body) +} + +// ok: body wrapped with LimitReader +func test_fp_limitreader(w http.ResponseWriter, r *http.Request) { + limited := io.LimitReader(r.Body, 1<<20) + body, _ := io.ReadAll(limited) + w.Write(body) +} + +// ok: reading from a non-HTTP source +func test_fp_non_http() { + resp, _ := http.Get("http://example.com") + body, _ := io.ReadAll(resp.Body) + fmt.Println(string(body)) +} + +func main() { + http.HandleFunc("/raw", test_readall_raw) + http.HandleFunc("/ioutil", test_ioutil_readall_raw) + http.HandleFunc("/var", test_readall_via_var) + http.HandleFunc("/maxbytes", test_fp_maxbytes) + http.HandleFunc("/limit", test_fp_limitreader) + test_fp_non_http() +} diff --git a/go/test/query-tests/security/UnboundedIORead/UnboundedIORead.qlref b/go/test/query-tests/security/UnboundedIORead/UnboundedIORead.qlref new file mode 100644 index 0000000..4c47823 --- /dev/null +++ b/go/test/query-tests/security/UnboundedIORead/UnboundedIORead.qlref @@ -0,0 +1 @@ +security/UnboundedIORead/UnboundedIORead.ql diff --git a/go/test/query-tests/security/UnboundedIORead/go.mod b/go/test/query-tests/security/UnboundedIORead/go.mod new file mode 100644 index 0000000..ecb8819 --- /dev/null +++ b/go/test/query-tests/security/UnboundedIORead/go.mod @@ -0,0 +1,3 @@ +module codeql-go-tests/query/UnboundedIORead + +go 1.18