fuck cors article

This commit is contained in:
Yann Esposito (Yogsototh) 2024-03-08 11:18:42 +01:00
parent 530645cae8
commit 92d7c0e6bd
Signed by untrusted user who does not match committer: yogsototh
GPG key ID: 7B19A4C650D59646

View file

@ -0,0 +1,197 @@
#+title: A quick CORS proxy in a few lines of Clojure
#+description:
#+keywords: clojure CORS web
#+author: Yann Esposito
#+email: yann@esposito.host
#+date: [2024-03-08 Fri]
#+lang: en
#+options: auto-id:t
#+startup: showeverything
I use a HTML file from one of my local directory as homepage.
This HTML file contains in a single one page view, all links I generally want to
jump to.
One of the section of links in this homepage contain a few website I host.
And I wanted to query these websites to make a healthcheck from my file.
It turns out that you cannot easily make an HTTP call to any external website
from a `file://` in your Browser as your are almost immediately blocked by CORS.
I don't want to explain how CORS are working, most people don't get it correctly
anyway.
The important point is that it is a security measure that is *very easy* to circumvent.
Here is how to do it:
1. Create a webservice that will play a role of "proxy". For example, the
webservice will read the ~?url=DESTINATION_URL~.
2. When receiving a request, the server will make a similar HTTP call to
~DESTINATION_URL~ and keep track of the ~Origin~ HTTP header of the request.
3. Take the response from ~DESTINATION_URL~ and add a few headers, in particular
add the ~Access-Control-Allow-Origin~ header that will contain the value in the
~Origin~ header of the request.
That's it.
So I wrote that in few minutes and use it now. So I can make these call and
detect when I see my homepage if one of my hosted website is not reachable.
* The Clojure code
:PROPERTIES:
:CUSTOM_ID: the-clojure-code
:END:
Here is the code in a few lines of Clojure:
#+begin_src clojure
(ns fuck-cors-app.core
(:require
[clj-http.client :as client]
[ring.adapter.jetty :as jetty]
[ring.middleware.params :refer [wrap-params]]
[fuck-cors.core :refer [wrap-open-cors]])
(:gen-class))
(defn handler
[request]
(if-let [url (get-in request [:query-params "url"])]
(client/request {:request-method (:request-method request)
:url url})
{:status 200
:headers {"Content-Type" "text/plain; charset=utf-8"}
:body "Let's bypass CORS ok?"}))
(defn -main
[& _args]
(jetty/run-jetty
(-> handler
(wrap-params)
(wrap-open-cors))
{:port 1977
:host "127.0.0.1"}))
#+end_src
And that's it, this is a whole web application that will proxy any call to a
website that do not allow you to call from some origin (like my `file://`) and
will make it work anyway.
If you feel that using too many libraries is cheating, here is the actual almost full
content of the lib taking care of handling CORS:
#+begin_src clojure
(defn- host-from-req
[request]
(str (-> request :scheme name)
"://"
(get-in request [:headers "host"])))
(defn- get-header
[request header-name]
(let [rawref (get-in request [:headers header-name])]
(if rawref
(clojure.string/replace rawref #"(http://[^/]*).*$" "$1")
nil)))
(defn wrap-open-cors
"Open your Origin Policy to Everybody, no limit"
[handler]
(fn [request]
(let [origin (get-header request "origin")
referer (get-header request "referer")
host (host-from-req request)
origins (if origin
origin
(if referer
referer
host))
headers {"Access-Control-Allow-Origin" origins
"Access-Control-Allow-Headers" "Origin, X-Requested-With, Content-Type, Accept, Cache-Control, Accept-Language, Accept-Encoding, Authorization"
"Access-Control-Allow-Methods" "HEAD, GET, POST, PUT, DELETE, OPTIONS, TRACE"
"Access-Control-Allow-Credentials" "true"
"Access-Control-Expose-Headers" "content-length"
"Vary" "Accept-Encoding, Origin, Accept-Language"}]
(-> (handler request)
(update-in [:headers] #(into % headers))))))
(defn wrap-preflight
"Add a preflight answer. Will break any OPTIONS handler, beware.
To put AFTER wrap-open-cors"
[handler]
(fn [request]
(if (= (request :request-method) :options)
(into request {:status 200 :body "preflight complete"})
(handler request))))
#+end_src
I wrote it a long time ago, and I think I just found a potential bug related to the headers.
I should probably retrieve all headers returned by the response, and add these
header name to the ~Access-Control-Allow-Headers~. But this list of allowed
headers will work most of the time.
* Bonus frontend code to check the availability of a website
:PROPERTIES:
:CUSTOM_ID: bonus-frontend-code-to-check-the-availability-of-a-website
:END:
As a bonus here is the code I use in my homepage to see if the website I am
looking for are reachable or not.
Imagine you have an HTML block like this:
#+begin_src html
<div class="healthcheck">
...
<a href="SOME_URL">website 1</a>
...
<a href="SOME_URL">website 2</a>
...
</div>
#+end_src
I have a CSS rule that change the background of these link to green or red if I
add the class ~ok~ or ~error~ to the ~<a>~.
And here is the javascript code:
#+begin_src javascript
// You can replace corsproxy.org (which is a public one)
// by the one you are hoting.
const corsproxyurl='https://corsproxy.org/?';
async function healthchecklink(a) {
var linkurl=a.href;
if (linkurl != undefined) {
var url = corsproxyurl + encodeURIComponent(linkurl);
try {
var response = await fetch(url, {method: 'GET',
redirect: 'manual',
signal: AbortSignal.timeout(3000)
});
if (response.ok || response.redirected || response.status === 0 ) {
a.classList.add("ok");
} else {
a.classList.add("error");
}
} catch (err) {
a.classList.add("error");
}
}
}
function checkhealth() {
var links = document.querySelectorAll('.healthcheck a');
for (l in links) {
healthchecklink(links[l]);
}
}
checkhealth();
#+end_src
Notice the ~response.status == 0~, this is due to one of my website returning a
303 redirection but some complication make it returns a status 0.
If there were an error it would return an error status.
That's it.
Another tool I created myself to prevent me using a service checking for the
status of my website and sending me notifications about it.
None of my website is crucial enough not be ok to wait a few hours to be re-enabled.