Skip to content

Commit

Permalink
Merge pull request #286 from desci-labs/m0ar/proxy-wash-response-secrets
Browse files Browse the repository at this point in the history
reverse-proxy: add support for washing secrets in response bodies
  • Loading branch information
m0ar authored Apr 18, 2024
2 parents a9d43db + 2b5695f commit 5dd4bdf
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 19 deletions.
6 changes: 6 additions & 0 deletions reverse-proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ So, in general, the pattern is `PROXY_MAPPING_[redirect key]=[redirect target]`.
| `/example` | https://example.com |
| `/example/cats` | https://example.com/cats |


## Washing responses
Some targets (looking at you, Alchemy) return the URL secrets in the response body. Since this is target-dependent, you need to add detection code in `getSensitiveStrings` if this applies to a new proxy target.

The request interceptor automatically washes all response bodies that goes back to the client by replacing instances of all these sensitive strings with `[redacted]`.

## Running
Run in development mode:
```shell
Expand Down
39 changes: 20 additions & 19 deletions reverse-proxy/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import express from 'express';
import { ServerResponse } from 'http';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware';
import { buildMappingFromEnv, getSensitiveStrings, redactSensitive } from './util.js';

const SENSIBLE_KEY_REGEX = '^\/[a-zA-Z0-9_-]+';
const rSensibleKey = RegExp(SENSIBLE_KEY_REGEX);

type Mapping = Record<string, string>;

const mapping: Mapping = Object.fromEntries(
Object.entries(process.env as { [s:string]: string } )
.filter(([k, _]) => k.startsWith("PROXY_MAPPING_"))
.map(([k, v]) => [k.replace("PROXY_MAPPING_", ""), v])
.map(([k, v]) => ["/" + k.toLowerCase(), v])
);
const mapping = buildMappingFromEnv();

console.log(
"Raw mapping configuration loaded:\n",
JSON.stringify(mapping, undefined, 2)
);

const sensitiveStrings = getSensitiveStrings(mapping);

/**
* Checks that a single mapping has a sensible key and that the
* target is a valid URL.
Expand Down Expand Up @@ -49,16 +45,21 @@ const createProxy = (target: string) => createProxyMiddleware({
pathRewrite: { [SENSIBLE_KEY_REGEX]: "" },
// Log each proxy call to console
logger: console,
// Disable automatically sending response, handled by responseInterceptor
selfHandleResponse: true,
on: {
error: (_err, _req, res) => {
if (res instanceof ServerResponse) {
res.writeHead(500, {
'Content-Type': 'text/plain',
});
res.end('Something went wrong while proxying the request');
};
error: (_err, _req, res) => (res as ServerResponse)
.writeHead(500, { 'Content-Type': 'text/plain' })
.end('Something went wrong while proxying the request'),
proxyRes: responseInterceptor(
async (responseBuffer, proxyRes, _req, res) => {
res.statusCode = proxyRes.statusCode ?? 200;
return redactSensitive(
sensitiveStrings,
responseBuffer.toString("utf8")
);
}),
},
}
});

const app = express();
Expand All @@ -73,8 +74,8 @@ app.use("/healthcheck", (_req, res) => {
app.use(
(req, res) => {
console.log(`Got request for unmapped path ${req.url}, responding 404.`);
return res.status(404).json(`No route found for ${req.url}`)
}
res.writeHead(404, `No route found for ${req.url}`).end();
},
);

app.listen(5678);
38 changes: 38 additions & 0 deletions reverse-proxy/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export type Mapping = Record<string, string>;

/**
* Build a route mapping from envvars prefixed `PROXY_MAPPING_`
*/
export const buildMappingFromEnv = (): Mapping => Object.fromEntries(
Object.entries(process.env as { [s:string]: string } )
.filter(([k, _]) => k.startsWith("PROXY_MAPPING_"))
.map(([k, v]) => [k.replace("PROXY_MAPPING_", ""), v])
.map(([k, v]) => ["/" + k.toLowerCase(), v])
);

/**
* Find sensitive strings in mapped URL's
*/
export const getSensitiveStrings = (mapping: Mapping) => {
const urls = Object.values(mapping);

const alchemyTokens = urls
.filter(url => url.includes("alchemy.com"))
/* 32-char alphanum token with dashes and underscores */
.map(url => url.match(/[a-zA-Z0-9_-]{32}/))
/* Remove potential null match for tokenless url */
.flatMap(maybeMatch => maybeMatch ? [maybeMatch] : [])
/* Get the match from RexExpMatchArray */
.map(matchArr => matchArr[0]);

return alchemyTokens;
};

export const redactSensitive = (
sensitiveStrings: string[],
dirty: string
): string =>
sensitiveStrings.reduce(
(acc, nextSecret) => acc.replaceAll(nextSecret, "[redacted]"),
dirty, // initial accumulator
);

0 comments on commit 5dd4bdf

Please sign in to comment.