How I added daphne
server dectection to wappalyzer
Table of contents
Back in the end of 2021 I was really amazed by wappalyzer
. This seemingly small piece of extension can detect what the backend is ( albeit not guranteed ).
As I was relatively new to django
back then I was amazed that many of the top sites are powered by django ( ・┆✦ʚ♡ɞ✦ ┆・ )!!! This was honestly a shock to me as most of people around me are talking all about PHP and stuff ( I mean PHP is not a bad language | Just gives too many opportunity to shoot yourself in the foot with bad practices )
Context¶
django
is a great framework written in python. It is robust, secure and generally pleasent to work with. The ASGI ( Asynchronus Server Gateway Interface ) framework written by django core team ( speally andrew
) was daphne
. dapnhe
was not detected by wappalyzer. This was annoying (ミ ̄ー ̄ミ)
Modifying Wappalyzer¶
So being annoyed at daphne
not being detected by wappalyzer
( which I mean it should ). I opened a pull request in the wappalyzer repo. Which modified these lines
"Daphne": {
"cats": [
22
],
"headers": {
"Server": "Daphne"
},
"implies":[
"TwistedWeb",
"Python",
"Zope"
],
"oss": true,
"icon": "daphne.svg",
"website": "https://github.com/django/daphne"
},
The Pull Request was accepted by @aliasio but I was shocked to see that daphne
was still not detected by wappalyzed
. Then diving deeper, It was being detected on my social media project but not on my other project. Panic seized me as I was frantically looking for answers. It couldn't be happening cause I saw that server
header detection was how wappalyzer
understood the backend software. I was pretty much sure that I saw daphne
on the server header on my chat application.
But then i realized that the header was only being sent on websocket
responses but not on http
responses.
Opening an issue on github¶
I was quick to go to django daphne
github to open an issue to investigate deeper into this mess. I thought that django
was hiding daphne
header intentionally to block out attackers ( this is not a good way to handle such use cases tbh ).
@adamchainz was quick to respond to me. Confirming that I was right on thinking that django
daphne
intentionally ( or maybe we had some sort of misunderstanding ).
Regardless I wanted the web to be open ( and by a large means allow others to see what they are doing ). So i tried defending my point. By pointing out that other web servers do they same and why should django
daphne
do things differently ? (django
+ daphne
wasn't event that common | django
+ uvicorn
was back then)
Then i pointed out other web servers doing things differently:
-
Gunicorn :
- Captured from : https://launchpad.net/
-
Uvicorn :
- Ran my application with
uvicorn core.asgi:application
- Ran my application with
So why was daphne
( a less popular server doing things differently )? Also they were already exposing server
headers via websockets
!!!
I countered @adamchainz argument
Adam : since it allows targeting servers when vulnerabilities become known
Me : I totally agree with this. So we can add an option ( or use a django middleware to modify the headers ) to disable header for mission critical servers. eg:if SERVER_HEADER_HIDDEN: headers.pop('Server')
Adam could finally understand me and he proposed a Pull Request to resolve this issue.
I was thrilled to get a chance into working into django
.. I mean it's django
. The most popular web framework for python
!!
Opening a Pull Request to resolve this issue¶
When I looked into daphne
source code. I came to realization that they are using twisted
for their network stack. They were using autobahn
for their websocket stack. Some poor chap before me modified the autobhan
header to be daphne
header and thats where I was getting the awkward server
header thingy.
I came across a piece of code that was doing nothing but it felt like was doing something. ( the server header will not be daphne duh )
if self.server.server_name and self.server.server_name.lower() != "daphne":
self.setHeader(b"server", self.server.server_name.encode("utf-8"))
I replaced it with my own custom code
if self.server.server_name and not self.responseHeaders.hasHeader("server"):
self.setHeader(b"server", self.server.server_name.encode())
and low and behold things were looking very promising to begin with.
Then adam reviewed my code once again.. This change was not done by me. It was done by black!!!
After some discussion I was able to resolve this issue and then ran into another. I have never written tests for software ( now that i am a bit older i know how important tests are ) So once again I was down a wild goose chase figuring out how to fix the tests.
After some digging around i wrote the tests
def test_no_servername(self):
"""
Passing `--no-server-name` will set server name to '' (empty string)
"""
self.assertCLI(["--no-server-name"], {"server_name": ""})
And this one too ( though adam helped me here )
return sorted(
[(b"server", b"daphne")]
+ [
(name.lower(), value.strip())
for name, value in headers
if name.lower() not in (b"server", b"transfer-encoding")
]
)
I then rewrote the CHANGELOG
but ran into another roadblock. My wording was not good ?
My wording : Added default headers to server response, so that if a response from backend didn't contain any server header it will automatically add a daphne server header.
Adam's wording : Always add the `Server` header, with value `Daphne`, when it isn’t already present.
This didn't sound good to me. I counteracted with my own points.
The header is hard coded for Websocket . We cannot change it from backend. ( Even if backend sends a header for websocket daphne overrides that | Thus it wasn't a correct implementation ). Check code and Issue and the pull request to change it. Because when i profiled daphne, server header was a mask over Autobahn. Otherwise daphne reports that it's running AutoBahn and Twisted.
Then Carlton
dropped in and shared his view. As I feared he thought this can be used for malicious intent.
I again hopped in and defended my view. I atleast want to give developers who share my view of a open web a chance to show their stack. ( this wasn't even possible as daphne would filter out the server
header )
Some chap before me tried to fix this issue. But he couldn't get it right and for some reason andrew
thought things were fine.
Then adam
hoped in and saw my perspective.
I understood the plan and gave developers the option to set custom server name or ( none if they prefer ) set a default one if there was no argument give.
But still to my avail the Pull Request was not merged. At this point I thought there was nothing more I can do but to give them time.
After 2 months I again pinged carlton for a review. If this will ever get merged or will this get closed or not.
He disappeared again. At this point i moved away from this issue and continued my life ( as i had an important exam in December and i couldn't afford to waste time like this ) but to my surprise near april the PR was merged. This was unexpected to be honest and i was overwhelmed with joy.
Thats how I got my first PR merged into django
daphne