Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Added modeling for the `extra_response_headers` constructor argument and instance attribute of `http.server.SimpleHTTPRequestHandler`, introduced in Python 3.15.
28 changes: 28 additions & 0 deletions python/ql/lib/semmle/python/frameworks/Stdlib.qll
Original file line number Diff line number Diff line change
Expand Up @@ -2041,6 +2041,34 @@ module StdlibPrivate {
override predicate valueAllowsNewline() { any() }
}

/** Additional response headers supplied by construction or instance assignment. */
private class ExtraHeadersWrite extends Http::Server::ResponseHeaderBulkWrite::Range {
DataFlow::Node headers;

ExtraHeadersWrite() {
exists(API::Node cls |
cls =
API::moduleImport("http")
.getMember("server")
.getMember("SimpleHTTPRequestHandler")
.getASubclass*()
|
this = cls.getACall() and
headers = this.(DataFlow::CallCfgNode).getArgByName("extra_response_headers")
or
this.(DataFlow::AttrWrite)
.writes([cls.getAnInstance(), cls.getAMember().getSelfParameter()]
.getAValueReachableFromSource(), "extra_response_headers", headers)
Comment thread
tausbn marked this conversation as resolved.
)
}

override DataFlow::Node getBulkArg() { result = headers }

override predicate nameAllowsNewline() { any() }

override predicate valueAllowsNewline() { any() }
}

private class AdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
nodeFrom = instance() and
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from http.server import BaseHTTPRequestHandler, SimpleHTTPRequestHandler
import http.server


def constructors(request, address, server, headers):
SimpleHTTPRequestHandler(request, address, server, extra_response_headers=headers) # $ headerWriteBulk=headers headerWriteBulkUnsanitized=name,value
http.server.SimpleHTTPRequestHandler(request, address, server, extra_response_headers=headers) # $ headerWriteBulk=headers headerWriteBulkUnsanitized=name,value
handler = SimpleHTTPRequestHandler
handler(request, address, server, extra_response_headers=headers) # $ headerWriteBulk=headers headerWriteBulkUnsanitized=name,value

SimpleHTTPRequestHandler(request, address, server, extra_response_headers=[("X-Example", "value")]) # $ headerWriteBulk=List headerWriteBulkUnsanitized=name,value headerWriteNameUnsanitized="X-Example" headerWriteValueUnsanitized="value"
SimpleHTTPRequestHandler(request, address, server)


class CustomHandler(SimpleHTTPRequestHandler):
def change_headers(self, headers):
self.extra_response_headers = headers # $ headerWriteBulk=headers headerWriteBulkUnsanitized=name,value
alias = self
alias.extra_response_headers = [("X-Alias", "value")] # $ headerWriteBulk=List headerWriteBulkUnsanitized=name,value headerWriteNameUnsanitized="X-Alias" headerWriteValueUnsanitized="value"
ensure_tainted(alias.headers) # $ tainted
ensure_not_tainted(alias.extra_response_headers)

def delegate(self, headers):
write_simple_headers(self, headers)


class DerivedHandler(CustomHandler):
def change_again(self, headers):
self.extra_response_headers = headers # $ headerWriteBulk=headers headerWriteBulkUnsanitized=name,value


def constructed_instance(request, address, server, headers):
handler = SimpleHTTPRequestHandler(request, address, server)
handler.extra_response_headers = headers # $ headerWriteBulk=headers headerWriteBulkUnsanitized=name,value
write_simple_headers(handler, headers)


def write_simple_headers(handler, headers):
alias = handler
alias.extra_response_headers = headers # $ headerWriteBulk=headers headerWriteBulkUnsanitized=name,value


def annotated_instance(handler: SimpleHTTPRequestHandler, headers):
handler.extra_response_headers = headers # $ headerWriteBulk=headers headerWriteBulkUnsanitized=name,value


class InheritedInitHandler(SimpleHTTPRequestHandler):
pass


def inherited_constructor(request, address, server, headers):
InheritedInitHandler(request, address, server, extra_response_headers=headers) # $ headerWriteBulk=headers headerWriteBulkUnsanitized=name,value


class BaseOnlyHandler(BaseHTTPRequestHandler):
def change_headers(self, headers):
# BaseHTTPRequestHandler does not consume this attribute.
self.extra_response_headers = headers
write_base_headers(self, headers)


def write_base_headers(handler, headers):
alias = handler
alias.extra_response_headers = headers


class Unrelated:
def __init__(self, *, extra_response_headers):
self.extra_response_headers = extra_response_headers


def unrelated(headers):
Unrelated(extra_response_headers=headers)
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#select
| extra_response_headers.py:9:52:9:56 | ControlFlowNode for value | extra_response_headers.py:5:16:5:19 | ControlFlowNode for self | extra_response_headers.py:9:52:9:56 | ControlFlowNode for value | This HTTP header is constructed from a $@. | extra_response_headers.py:5:16:5:19 | ControlFlowNode for self | user-provided value |
| extra_response_headers.py:11:42:11:45 | ControlFlowNode for name | extra_response_headers.py:5:16:5:19 | ControlFlowNode for self | extra_response_headers.py:11:42:11:45 | ControlFlowNode for name | This HTTP header is constructed from a $@. | extra_response_headers.py:5:16:5:19 | ControlFlowNode for self | user-provided value |
| extra_response_headers.py:27:49:27:53 | ControlFlowNode for value | extra_response_headers.py:21:16:21:19 | ControlFlowNode for self | extra_response_headers.py:27:49:27:53 | ControlFlowNode for value | This HTTP header is constructed from a $@. | extra_response_headers.py:21:16:21:19 | ControlFlowNode for self | user-provided value |
| flask_tests.py:13:17:13:26 | ControlFlowNode for rfs_header | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:13:17:13:26 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
| flask_tests.py:20:36:20:61 | ControlFlowNode for Subscript | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:20:36:20:61 | ControlFlowNode for Subscript | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
| flask_tests.py:33:11:33:20 | ControlFlowNode for rfs_header | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:33:11:33:20 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
Expand All @@ -7,6 +10,12 @@
| wsgiref_tests.py:8:17:8:22 | ControlFlowNode for h_name | wsgiref_tests.py:4:14:4:20 | ControlFlowNode for environ | wsgiref_tests.py:8:17:8:22 | ControlFlowNode for h_name | This HTTP header is constructed from a $@. | wsgiref_tests.py:4:14:4:20 | ControlFlowNode for environ | user-provided value |
| wsgiref_tests.py:8:42:8:46 | ControlFlowNode for h_val | wsgiref_tests.py:4:14:4:20 | ControlFlowNode for environ | wsgiref_tests.py:8:42:8:46 | ControlFlowNode for h_val | This HTTP header is constructed from a $@. | wsgiref_tests.py:4:14:4:20 | ControlFlowNode for environ | user-provided value |
edges
| extra_response_headers.py:5:16:5:19 | ControlFlowNode for self | extra_response_headers.py:6:9:6:12 | ControlFlowNode for name | provenance | AdditionalTaintStep |
| extra_response_headers.py:5:16:5:19 | ControlFlowNode for self | extra_response_headers.py:7:9:7:13 | ControlFlowNode for value | provenance | AdditionalTaintStep |
| extra_response_headers.py:6:9:6:12 | ControlFlowNode for name | extra_response_headers.py:11:42:11:45 | ControlFlowNode for name | provenance | |
| extra_response_headers.py:7:9:7:13 | ControlFlowNode for value | extra_response_headers.py:9:52:9:56 | ControlFlowNode for value | provenance | |
| extra_response_headers.py:21:16:21:19 | ControlFlowNode for self | extra_response_headers.py:23:9:23:13 | ControlFlowNode for value | provenance | AdditionalTaintStep |
| extra_response_headers.py:23:9:23:13 | ControlFlowNode for value | extra_response_headers.py:27:49:27:53 | ControlFlowNode for value | provenance | |
| flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:1:29:1:35 | ControlFlowNode for request | provenance | |
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:9:18:9:24 | ControlFlowNode for request | provenance | |
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:19:18:19:24 | ControlFlowNode for request | provenance | |
Expand Down Expand Up @@ -37,6 +46,14 @@ models
| 1 | Summary: urllib; Member[parse].Member[parse_qs]; Argument[0,qs:]; ReturnValue; taint |
| 2 | Summary: urllib; Member[parse].Member[urlparse]; Argument[0,urlstring:]; ReturnValue; taint |
nodes
| extra_response_headers.py:5:16:5:19 | ControlFlowNode for self | semmle.label | ControlFlowNode for self |
| extra_response_headers.py:6:9:6:12 | ControlFlowNode for name | semmle.label | ControlFlowNode for name |
| extra_response_headers.py:7:9:7:13 | ControlFlowNode for value | semmle.label | ControlFlowNode for value |
| extra_response_headers.py:9:52:9:56 | ControlFlowNode for value | semmle.label | ControlFlowNode for value |
| extra_response_headers.py:11:42:11:45 | ControlFlowNode for name | semmle.label | ControlFlowNode for name |
| extra_response_headers.py:21:16:21:19 | ControlFlowNode for self | semmle.label | ControlFlowNode for self |
| extra_response_headers.py:23:9:23:13 | ControlFlowNode for value | semmle.label | ControlFlowNode for value |
| extra_response_headers.py:27:49:27:53 | ControlFlowNode for value | semmle.label | ControlFlowNode for value |
| flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| flask_tests.py:9:5:9:14 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from http.server import BaseHTTPRequestHandler, SimpleHTTPRequestHandler


class ExtraHeadersHandler(SimpleHTTPRequestHandler):
def do_GET(self):
name = self.headers["X-Header-Name"]
value = self.headers["X-Header-Value"]

self.extra_response_headers = [("X-Value", value)] # BAD
alias = self
alias.extra_response_headers = [(name, "value")] # BAD

clean_name = name.replace("\r", "").replace("\n", "")
clean_value = value.replace("\r", "").replace("\n", "")
self.extra_response_headers = [(clean_name, clean_value)] # GOOD
self.extra_response_headers = [("X-Static", "value")] # GOOD
super().do_GET()


class DispatchHandler(BaseHTTPRequestHandler):
def do_GET(self):
name = self.headers["X-Header-Name"]
value = self.headers["X-Header-Value"]

SimpleHTTPRequestHandler(
self.request, self.client_address, self.server,
extra_response_headers=[("X-Value", value)], # BAD
)
# BaseHTTPRequestHandler does not consume this attribute.
self.extra_response_headers = [(name, value)] # GOOD
Loading