aleph

rgm 2024-05-03T22:14:31.146039Z

Hi all, I found a tantalizing discussion around using the :pipeline-transform opt on aleph.http/start-server to strip a Server: header and I'm hoping someone might point out what I'm doing wrong in implementing it: https://github.com/clj-commons/aleph/issues/512#issuecomment-695972882 Background: I'm cleaning up from a pentest and the consultant would prefer no Server: header at all. I've added a custom header in a middleware to just suppress the phrase "Aleph" for now and that's also fine. But I've tried working out how to do a :pipeline-transform and I'm just not getting it. My attempt w/ failing unit test is here: https://github.com/rgm/experiments/blob/master/2024/202405-aleph/user.clj (It's still Aleph/0.4.7; I know this is gonna be an issue where the pipeline keys have changed if we try to come up to 0.7).

Matthew Davidson 2024-05-08T10:23:11.178329Z

Bit late to the party, but if you're trying to understand how Netty handlers work, it helps to know that, at any time, you can write from the inbound handler chain to the outbound chain. This short-circuits invocations on all later handlers in the chain, and is usually a good idea for efficiency. Aleph wraps the user handler with its own netty-compatible handler, which takes your Ring response, adds the Server header (among others), and in 0.4.x, starts writing Http* objects. This calls the previous handler's .write method and works it way back in reverse. This is why .addBefore is what you want. My suggestion is to insert a netty/channel-outbound-handler and set the :write so that if it gets an HttpMessage, it alters the .headers, and passes it along. If it doesn't get an HttpMessage, it should just pass the object along (the general Netty strategy).

Matthew Davidson 2024-05-08T10:31:03.162139Z

Modern Aleph will only set the server name if the header is missing, but that still doesn't help if you want to remove it entirely. Because of how the underlying Netty Channel multiplexes HTTP/2 connections (one shared connection-level channel, and then one channel for each HTTP/2 stream ID), the pipeline-transform keys are different, as you noticed. You'd want to .addBefore the handler again, but you want to use the http2-stream-pipeline-transform key this time

Matthew Davidson 2024-05-08T10:32:19.592829Z

(NB: that's for HTTP/2 servers. If you have an HTTP/1 server, it's still pipeline-transform)

rgm 2024-05-08T15:38:21.045009Z

Oh, this is fantastic. Thank you. It all looked a bit interceptor-y.

Matthew Davidson 2024-05-08T15:57:20.915279Z

Yeah, there's a lot of similarities, but fyi, that pattern for pipelines is older than the use of the term interceptor or the InterceptingFilter. Netty in particular predates Pedestal's popularization of the word. I dug a bit into the history of it all at one point. See my comment here: https://clojurians.slack.com/archives/CDG3YQV6Z/p1685076301109489

🤯 1
rgm 2024-05-08T16:04:42.359319Z

ok I'm so glad I pressed on past the 2-minute solution of (assoc-in request [:headers "Server"] "*REDACTED*")

👍 1
valerauko 2024-05-05T02:41:19.249159Z

If it's a pentest I'd personally add something like Server: Django/5.7 (Python/3.2) (look up the actual django string) and enjoy the onslaught

rgm 2024-05-07T18:22:54.508799Z

Looks like I get "Server:\n" from curl

rgm 2024-05-03T22:17:12.358639Z

I'm vaguely understanding I should proxy io.netty.Channel/ChannelOutboundHandlerAdapter because it has default methods, but I'm not at all convinced its .write method is the right one to override.

oyakushev 2024-05-03T22:28:39.547939Z

Try printlning the pipeline after you've added the scrubber. What does that show? Also, do your debug printlns get invoked?

oyakushev 2024-05-03T22:31:15.292589Z

Actually, not just printling the object, but (println (.names pipeline))

rgm 2024-05-03T22:33:30.933409Z

hm, no ... nothing prints. I ran the 2 forms in the comment, output on the right. Looking at the start-server docstring for 0.4.7 I seem to be lining up the :pipeline-transform keyword OK. Wonder if I'm not matching a method arity someplace.

rgm 2024-05-03T22:35:43.113839Z

oh, wait 🤦 ... output is going out to the REPL but my editor's not picking up output on some other thread

oyakushev 2024-05-03T22:35:55.649539Z

Yeah, just wanted to say that

oyakushev 2024-05-03T22:36:03.933609Z

Happens to the best of us 🙃

rgm 2024-05-03T22:36:54.605929Z

ok, so this is the pre- and post- (.names pipeline)

rgm 2024-05-03T22:37:16.733629Z

so the transform is running

oyakushev 2024-05-03T22:37:53.095699Z

Try putting the scrubber before http-server

👍 1
oyakushev 2024-05-03T22:38:19.052629Z

(.addBefore pipeline "http-server" "scrubber" SCRUBBER)

rgm 2024-05-03T22:38:52.240179Z

huh, just after I pasted the last one, I got all this warning logging having done nothing else in the meantime

oyakushev 2024-05-03T22:39:54.360569Z

Yes, you got to instantiate that handler anew on each pipeline-transform

oyakushev 2024-05-03T22:39:59.166559Z

make it a defn instead of def

rgm 2024-05-03T22:40:18.419799Z

ohhhh, ok

rgm 2024-05-03T22:43:05.298059Z

getting closer ... the write method is actually running now

rgm 2024-05-03T22:43:41.274489Z

oh ... bad call to proxy-super

rgm 2024-05-03T22:45:20.753349Z

pretty close, the issue now is that the (when (instance? HttpResponse ,,,)) isn't the right thing to do

rgm 2024-05-03T22:45:36.892389Z

but now I can inspect, this is great

oyakushev 2024-05-03T22:45:41.482249Z

Println what's coming to you

rgm 2024-05-03T22:49:26.154519Z

cool ... I'll poke around a bit more with the netty class hierarchy but this is great progress, thank you

oyakushev 2024-05-03T22:50:36.228319Z

BTW, instead of proxy you can use this:

(aleph.netty/channel-outbound-handler
 :write ([_ ctx msg promise]
         (prn "running write method")
         (when (instance? HttpResponse msg)
           (prn "removing header")
           (.remove (.headers msg) "Server"))
         (.write ctx msg promise)))

👍 1
oyakushev 2024-05-03T22:51:09.366839Z

Is this .addBefore now or .addLast?

rgm 2024-05-03T22:51:15.257329Z

before

oyakushev 2024-05-03T22:51:32.099039Z

Then it probably should be .addLast still

oyakushev 2024-05-03T22:52:07.438339Z

Because since you receive a byte buffer as a msg, it means your handler got invoked too late in the chain

rgm 2024-05-03T22:52:34.975359Z

hm, as .addLast it doesn't get invoked at all

rgm 2024-05-03T22:52:43.588699Z

(the scrubber proxy)

oyakushev 2024-05-03T22:53:13.778409Z

Between middleware, interceptors, and Netty handlers, I completely give up trying to understand the order of invocation in these chain of responsibility atrocities.

rgm 2024-05-03T22:54:05.762099Z

yeah, I usually just have to set up mw1 and mw2 and watch the logs

rgm 2024-05-03T22:54:16.574579Z

no hope of remembering

oyakushev 2024-05-03T22:55:16.127869Z

Hm, stupid idea: maybe try .addFirst?

rgm 2024-05-03T22:56:42.548929Z

yep, runs as .addFirst too

rgm 2024-05-03T22:56:56.709769Z

rgm 2024-05-03T22:58:22.000069Z

I'll mess with (.addBefore ,,,) and see what happens

rgm 2024-05-03T22:58:35.052479Z

presumably there's somewhere in the chain that makes sense

rgm 2024-05-03T22:59:42.882779Z

gotta step away from the keyboard for a while but thanks so much ... this is big progress

👍 1
oyakushev 2024-05-03T22:59:54.468909Z

No problem

oyakushev 2024-05-03T23:00:14.597629Z

Try dancing around "request-handler", not "http-server", my initial suggestion was wrong

rgm 2024-05-03T23:02:32.096959Z

suppose I could just doseq on (.names pipeline) and stick it between all of them 🙃

rgm 2024-05-03T23:07:43.160259Z

ok now I'm late for my thing but TOTALLY WORTH IT ... (.addBefore pipeline "request-handler" "scrubber" (make-scrubber-2)) worked and makes the unit test pass 🎉

🔥 1
oyakushev 2024-05-03T23:09:49.828009Z

Yeah, this makes sense now... I confused http-server and request-handler. But think of the fascinating discoveries we made!

💝 1
rgm 2024-05-03T23:11:09.364549Z

I somehow went 8+ years of clojure without even knowing proxy existed and now this is twice in two months

oyakushev 2024-05-03T23:12:19.421099Z

It really isn't needed that often, so no wonder. And it's quite clunky and buggy too, with its proxy-super stuff.

rgm 2024-05-03T23:14:38.812109Z

</me files proxy in the same mental drawer that already has cljs this-as>

👍 1
rgm 2024-05-03T23:14:55.486649Z

have a great weekend and thanks again 👋

oyakushev 2024-05-03T23:15:13.807969Z

Cheers, likewise!

rgm 2024-05-06T15:07:27.418919Z

Ha. I did consider Server: DIAF/v1.0.0

valerauko 2024-05-06T15:18:52.038589Z

We regularly observe super hacker bros scanning our servers from some German VPS for vulnerabilities. It's hilarious because our people have given plenty of talks about our stack and we even have a tech blog about it, but no they are brute forcing PHP and ASP known weak points and common misconfigurations...

rgm 2024-05-06T16:10:38.164509Z

ha, same ... I asked our pen tester about that since I was seeing a lot of Rails/Express/Django/ASP probing in our error logs, and he said it's just in the big automatic checklist their tools run through. In the debrief he was really enthusiastic about learning new stuff since he'd never seen transit on the wire before.

rgm 2024-05-06T16:15:01.436769Z

But now that I think about it it does seem inconsistent to both advise us to turn off the Server: header and to also completely ignore it in practice, because tooling makes ignoring it the path of least resistance.

rgm 2024-05-06T16:17:30.762879Z

having it off almost feels like it'll get us fingerprinted as being possibly worth extra attention (which I doubt we are, but still. Bit like the Do Not Track header being used to help fingerprint my browser).

rgm 2024-05-06T16:18:59.318689Z

Anyway, whatevs. Unlikely to be the most troubling not-entirely-thought-through thing I encounter today.

valerauko 2024-05-06T23:53:47.593689Z

btw what happens if you set the Server header to an empty string explicitly in aleph? doesn't netty exclude it?