Tuesday, February 2, 2021

Injecting JavaScript Agent Using NGINX


 


In this post we will review how to inject JavaScript code to a site using NGINX.

We will review several methods:

  1. Static JavaScript code injection
  2. Dynamic JavaScript code injection
  3. Long dynamic JavaScript code injection



The Start Position


Lets assume that we have an NGINX running with the following configuration:


nginx.conf

user  nginx;
worker_processes 10;

error_log /dev/stdout debug;
pid /var/run/nginx.pid;

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/resolvers.conf;
access_log on;
sendfile on;
port_in_redirect off;

upstream backend {
server www.backend.com;
keepalive 1000;
keepalive_requests 1000000;
keepalive_timeout 300s;
}

server {
listen 8080;
server_name localhost;

location / {
proxy_set_header Accept-Encoding "";
proxy_set_header Host $http_host;
proxy_pass http://backend;
}
}
}



So we simply use the NGINX as a reverse proxy to an upstream server www.backend.com.

Next we will examine various methods to inject JavaScript code base on this setup.



Static JavaScript Code Injection


We want to inject a short JavaScript code, for example:



console.log('Hello World')



To inject the JavaScript, we update the following under the location / section in the NGINX configuration:


nginx.conf (partial view)

proxy_set_header            Accept-Encoding "";
sub_filter '<head>' '<head><script>console.log("Hello World")</script>';
sub_filter_once on;



We are using the substitute directive to replace the <head> of the result HTML. Notice that we have also used the Accept-Encoding header to ensure that the result from the upstream server will not be returned as a compressed HTML, and hence preventing from the substitute to locate the <head> tag.



Dynamic JavaScript Code Injection


Now we want the code to be dynamic, for example based on a MY_CLIENT_TOKEN header in the request, we will decide on the result injection code. If the MY_CLIENT_TOKEN header is valid, we will add the following script:



console.log('Welcome to my site. We will enable everything for you')



But if the MY_CLIENT_TOKEN header is invalid, we will inject the following JavaScript



console.log('Nothing is enabled for you yet')


To inject the JavaScript, we will use an NGINX auth request that will be sent to our own microservice. The auth microservice will generate a header including the dynamic JavaScript code. Let start by adding the auth upstream under the http section in the NGINX configuration.


nginx.conf (partial view)

upstream auth {
server my-auth-service;
keepalive 1000;
keepalive_requests 1000000;
keepalive_timeout 300s;
}



Next, we update the location to use auth request, and then get the script header from the auth response to be used as the replacement:



location /auth {
proxy_pass http://auth;
}

location / {
auth_request /auth;
auth_request_set $script $sent_http_script;

proxy_set_header Accept-Encoding "";
sub_filter '<head>' '<head><script>${script}</script>';
sub_filter_once on;

proxy_set_header Accept-Encoding "";
proxy_set_header Host $http_host;
proxy_pass http://backend;
}



Long Dynamic JavaScript Code Injection


So our dynamic injection works great, but at some point, we might have a very big java script code injection, and we get to a problem, as the maximum valid HTTP header size is 10KB.

To overcome this issue, we inject a script that loads our javascript. To make sure that the script is running as the first JavaScript on the HTML page, we can use the async=false attribute.

The NGINX configuration to handle this is the same as before, BUT instead of returning our actual JavaScript from the auth service, we return the following JavaScript:



var s = document.createElement('script')
s.src = '/auth/get-actual-script'
s.type = 'text/javascript'
s.async = false
document.getElementsByTagName('head')[0].appendChild(s)



We can dynamically decide on our auth service which script source to return. In the example above it is /auth/get-actual-script, and we can set this dynamically, for example according to the request headers.



Final Note


In this post we have reviewed method to inject JavaScript to an HTML page using NGINX as a reverse proxy. 

The static injection will be usually used for small modifications in the HTML page, when you cannot update the original backend server.

The dynamic injection will be usually used  for an additional logic before that backend, for example to implement another authentication layer.

The large dynamic will be usually used when a dynamic injection is used, and the JavaScript grow large, for example due to use of babel to support old browsers.


No comments:

Post a Comment