Skip to content

Commit f304994

Browse files
authored
feat: Add new ENV config HEADER_PREFIXES_ALLOWED (#241)
Adds the ability to create a list of header prefixes that are allowed to be returned to the client using the new `HEADER_PREFIXES_ALLOWED` configuration option which is a companion configuration option to `HEADER_PREFIXES_TO_STRIP`. If specified, this configuration value will take precedence over the default stripped header prefixes (`x-amz-`) as well as and prefixes specified in `HEADER_PREFIXES_TO_STRIP`. Note that it is generally not advised to return any object store specific headers to the client and the default remains to enforce stripping of these headers. Use this configuration option with caution.
1 parent de13fab commit f304994

File tree

4 files changed

+90
-4
lines changed

4 files changed

+90
-4
lines changed

common/docker-entrypoint.d/00-check-for-required-env.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,5 @@ echo "Directory Listing Path Prefix: ${DIRECTORY_LISTING_PATH_PREFIX}"
134134
echo "Provide Index Pages Enabled: ${PROVIDE_INDEX_PAGE}"
135135
echo "Append slash for directory enabled: ${APPEND_SLASH_FOR_POSSIBLE_DIRECTORY}"
136136
echo "Stripping the following headers from responses: x-amz-;${HEADER_PREFIXES_TO_STRIP}"
137+
echo "Allow the following headers from responses (these take precendence over the above): ${HEADER_PREFIXES_ALLOWED}"
137138
echo "CORS Enabled: ${CORS_ENABLED}"

common/etc/nginx/include/s3gateway.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ const S3_STYLE = process.env['S3_STYLE'];
7777
* @type {Array<String>}
7878
* */
7979
const ADDITIONAL_HEADER_PREFIXES_TO_STRIP = utils.parseArray(process.env['HEADER_PREFIXES_TO_STRIP']);
80+
81+
/**
82+
* Additional header prefixes to allow from the response before sending to the
83+
* client. This is opposite to HEADER_PREFIXES_TO_STRIP.
84+
* @type {Array<String>}
85+
* */
86+
const ADDITIONAL_HEADER_PREFIXES_ALLOWED = utils.parseArray(process.env['HEADER_PREFIXES_ALLOWED']);
87+
8088
/**
8189
* Default filename for index pages to be read off of the backing object store.
8290
* @type {string}
@@ -100,15 +108,19 @@ function editHeaders(r) {
100108
r.method === 'HEAD' &&
101109
_isDirectory(decodeURIComponent(r.variables.uri_path));
102110

103-
/* Strips all x-amz- headers from the output HTTP headers so that the
111+
/* Strips all x-amz- (if x-amz- is not in ADDITIONAL_HEADER_PREFIXES_ALLOWED) headers from the output HTTP headers so that the
104112
* requesters to the gateway will not know you are proxying S3. */
105113
if ('headersOut' in r) {
106114
for (const key in r.headersOut) {
115+
const headerName = key.toLowerCase()
107116
/* We delete all headers when it is a directory head request because
108117
* none of the information is relevant for passing on via a gateway. */
109118
if (isDirectoryHeadRequest) {
110119
delete r.headersOut[key];
111-
} else if (_isHeaderToBeStripped(key.toLowerCase(), ADDITIONAL_HEADER_PREFIXES_TO_STRIP)) {
120+
} else if (
121+
!_isHeaderToBeAllowed(headerName, ADDITIONAL_HEADER_PREFIXES_ALLOWED)
122+
&& _isHeaderToBeStripped(headerName, ADDITIONAL_HEADER_PREFIXES_TO_STRIP)
123+
) {
112124
delete r.headersOut[key];
113125
}
114126
}
@@ -145,6 +157,24 @@ function _isHeaderToBeStripped(headerName, additionalHeadersToStrip) {
145157
return false;
146158
}
147159

160+
/**
161+
* Determines if a given HTTP header should be force allowed from requesting client.
162+
* @param headerName {string} Lowercase HTTP header name
163+
* @param additionalHeadersToAllow {Array<string>} array of additional headers to allow
164+
* @returns {boolean} true if header should be removed
165+
*/
166+
function _isHeaderToBeAllowed(headerName, additionalHeadersToAllow) {
167+
168+
for (let i = 0; i < additionalHeadersToAllow.length; i++) {
169+
const headerToAllow = additionalHeadersToAllow[i];
170+
if (headerName.indexOf(headerToAllow, 0) >= 0) {
171+
return true;
172+
}
173+
}
174+
175+
return false;
176+
}
177+
148178
/**
149179
* Outputs the timestamp used to sign the request, so that it can be added to
150180
* the 'Date' header and sent by NGINX.

docs/getting_started.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,14 @@ running as a Container or as a Systemd service.
3939
| `PROXY_CACHE_VALID_FORBIDDEN` | No | | `30s` | Sets caching time for response code 403 |
4040
| `PROVIDE_INDEX_PAGE` | No | `true`, `false` | `false` | Flag which returns the index page if there is one when requesting a directory. |
4141
| `JS_TRUSTED_CERT_PATH` | No | | | Enables the `js_fetch_trusted_certificate` directive when retrieving AWS credentials and sets the path (on the container) to the specified path |
42-
| `HEADER_PREFIXES_TO_STRIP` | No | | | A list of HTTP header prefixes that exclude headers client responses. List should be specified in lower-case and a semicolon (;) should be used to as a deliminator between values. For example: `x-goog-;x-something-` |
42+
| `HEADER_PREFIXES_TO_STRIP` | No | | | A list of HTTP header prefixes that exclude headers from client responses. List should be specified in lower-case and a semicolon (;) should be used to as a deliminator between values. For example: x-goog-;x-something-. Headers starting with x-amz- will be stripped by default for security reasons unless explicitly added in HEADER_PREFIXES_ALLOWED. |
43+
| `HEADER_PREFIXES_ALLOWED` | No | | | A list of allowed prefixes for HTTP headers that are returned to the client in responses. List should be specified in lower-case and a semicolon (;) should be used to as a deliminator between values. For example: x-amz-;x-something-. It is NOT recommended to return x-amz- headers for security reasons. Think carefully about what is allowed here. |
4344
| `CORS_ENABLED` | No | `true`, `false` | `false` | Flag that enables CORS headers on GET requests and enables pre-flight OPTIONS requests. If enabled, this will add CORS headers for "fully open" cross domain requests by default, meaning all domains are allowed, similar to the settings show in [this example](https://enable-cors.org/server_nginx.html). CORS settings can be fine-tuned by overwriting the [`cors.conf.template`](/common/etc/nginx/templates/gateway/cors.conf.template) file. |
4445
| `CORS_ALLOWED_ORIGIN` | No | | | value to set to be returned from the CORS `Access-Control-Allow-Origin` header. This value is only used if CORS is enabled. (default: \*) |
4546
| `STRIP_LEADING_DIRECTORY_PATH` | No | | | Removes a portion of the path in the requested URL (if configured). Useful when deploying to an ALB under a folder (eg. www.mysite.com/somepath). |
4647
| `PREFIX_LEADING_DIRECTORY_PATH` | No | | | Prefix to prepend to all S3 object paths. Useful to serve only a subset of an S3 bucket. When used in combination with `STRIP_LEADING_DIRECTORY_PATH`, this allows the leading path to be replaced, rather than just removed. |
48+
|
49+
4750

4851
If you are using [AWS instance profile credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html),
4952
you will need to omit the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` variables from

test/unit/s3gateway_test.js

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,54 @@ function testEditHeaders() {
142142
}
143143

144144
s3gateway.editHeaders(r);
145-
145+
146146
for (const key in r.headersOut) {
147147
if (key.toLowerCase().indexOf("x-amz", 0) >= 0) {
148148
throw "x-amz header not stripped from headers correctly";
149149
}
150150
}
151151
}
152152

153+
function testEditHeadersWithAllowedPrefixes() {
154+
printHeader('testEditHeadersWithAllowedPrefixes');
155+
156+
process.env['HEADER_PREFIXES_ALLOWED'] = 'x-amz-'
157+
const r = {
158+
"headersOut": {
159+
"Accept-Ranges": "bytes",
160+
"Content-Length": 42,
161+
"Content-Security-Policy": "block-all-mixed-content",
162+
"Content-Type": "text/plain",
163+
"X-Amz-Bucket-Region": "us-east-1",
164+
"X-Amz-Request-Id": "166539E18A46500A",
165+
"X-Xss-Protection": "1; mode=block"
166+
},
167+
"variables": {
168+
"uri_path": "/a/c/ramen.jpg"
169+
},
170+
}
171+
172+
r.log = function(msg) {
173+
console.log(msg);
174+
}
175+
176+
s3gateway.editHeaders(r);
177+
178+
let found_headers_x_amz_ = 0
179+
for (const key in r.headersOut) {
180+
if (key.toLowerCase().indexOf("x-amz", 0) == 0) {
181+
found_headers_x_amz_++;
182+
}
183+
}
184+
185+
if (found_headers_x_amz_ != 2)
186+
throw "x-amz header stripped from headers, should allow those 2 headers";
187+
188+
delete process.env['HEADER_PREFIXES_ALLOWED']
189+
190+
}
191+
192+
153193
function testEditHeadersHeadDirectory() {
154194
printHeader('testEditHeadersHeadDirectory');
155195
let r = {
@@ -192,6 +232,18 @@ function testIsHeaderToBeStripped() {
192232
}
193233
}
194234

235+
function testIsHeaderToBeAllowed() {
236+
printHeader('testIsHeaderToBeAllowed');
237+
238+
if (!s3gateway._isHeaderToBeAllowed('x-amz-abc', ['x-amz-'])) {
239+
throw "x-amz-abc header should be allowed";
240+
}
241+
242+
if (s3gateway._isHeaderToBeAllowed('x-amz-xyz',['x-amz-abc'])) {
243+
throw "x-amz-xyz header should be stripped";
244+
}
245+
}
246+
195247
function testEscapeURIPathPreservesDoubleSlashes() {
196248
printHeader('testEscapeURIPathPreservesDoubleSlashes');
197249
var doubleSlashed = '/testbucketer2/foo3//bar3/somedir/license';

0 commit comments

Comments
 (0)