Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 4 additions & 20 deletions .github/workflows/docs.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,27 +23,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: 'latest'

- name: Install mdbook-mermaid
run: |
VERSION=$(curl -s https://api.github.com/repos/badboy/mdbook-mermaid/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -sSL "https://github.com/badboy/mdbook-mermaid/releases/download/${VERSION}/mdbook-mermaid-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" | tar -xz
sudo mv mdbook-mermaid /usr/local/bin/
mdbook-mermaid install .

- name: Install mdbook-linkcheck
run: |
curl -sSL https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/latest/download/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -o linkcheck.zip
unzip -q -o linkcheck.zip
chmod +x mdbook-linkcheck
sudo mv mdbook-linkcheck /usr/local/bin/

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Build documentation
run: mdbook build
run: ./scripts/mdbook.sh build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
Expand Down
2 changes: 1 addition & 1 deletion book.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ site-url = "https://coder.github.io/httpjail/"
cname = ""
mathjax-support = false
copy-fonts = true
additional-css = []
additional-css = ["docs/custom.css"]
additional-js = ["mermaid.min.js", "mermaid-init.js"]
no-section-label = false
fold.enable = true
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@

# Advanced

- [Request Body Limiting](./advanced/request-body-limiting.md)
- [TLS Interception](./advanced/tls-interception.md)
- [DNS Exfiltration](./advanced/dns-exfiltration.md)
- [Server Mode](./advanced/server-mode.md)
Expand Down
148 changes: 148 additions & 0 deletions docs/advanced/request-body-limiting.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
# Request Body Limiting

The `max_tx_bytes` feature allows you to limit the total size of HTTP requests sent to upstream servers.

This is primarily designed for mitigating code exfiltration attacks through covert channels.

## Size Calculation

The `max_tx_bytes` limit applies to **complete** HTTP requests, including:

1. **Request line**: `METHOD /path HTTP/1.1\r\n`
2. **Headers**: Each header as `Name: Value\r\n`
3. **Header separator**: Final `\r\n` between headers and body
4. **Body**: Request body bytes

## Response Format

To enable request body limiting, return an object with `max_tx_bytes` in your rule response:

```javascript
// JavaScript engine
{allow: {max_tx_bytes: 1024}} // Limit to 1KB total request size
```

```json
// Line processor engine
{"allow": {"max_tx_bytes": 1024}}
```

> **Note**: The `max_tx_bytes` feature is only available in the JavaScript (`--js`) and Line Processor (`--proc`) engines, not in Shell scripts.

## Behavior

The limiting behavior depends on whether the request includes a `Content-Length` header:

### With Content-Length Header

When the request includes a `Content-Length` header (most standard HTTP clients):

1. **Early Detection**: httpjail calculates the total request size
2. **Immediate Rejection**: If it exceeds `max_tx_bytes`, the client receives a `413 Payload Too Large` error immediately
3. **No Upstream Contact**: The upstream server is never contacted, preventing unnecessary load
4. **Clear Feedback**: The error message indicates the actual size and limit

**Example error response:**
```
HTTP/1.1 413 Payload Too Large
Content-Type: text/plain

Request body size (5000 bytes) exceeds maximum allowed (1024 bytes)
```

### Without Content-Length Header

When the request uses chunked encoding or doesn't include `Content-Length`:

1. **Stream Truncation**: The request body is truncated at the limit during streaming
2. **Upstream Receives Partial**: The upstream server receives exactly `max_tx_bytes` total bytes (url + headers + truncated body)
3. **Connection Closes**: The connection terminates after reaching the limit

## Examples

### JavaScript Engine - Upload Endpoint Limiting

```javascript
// Limit upload endpoints to 1KB total request size
const uploadHosts = ['uploads.example.com', 'upload.github.com'];

uploadHosts.includes(r.host)
? {allow: {max_tx_bytes: 1024}}
: r.host.endsWith('.example.com')
```

### Line Processor Engine - Python Example

```python
#!/usr/bin/env python3
import sys, json

upload_hosts = {'uploads.example.com', 'data.api.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
# Returns 413 error if Content-Length exceeds limit
# Truncates body if no Content-Length header
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
elif req['host'].endswith('.example.com'):
print("true")
else:
print("false")
except:
print("false")
sys.stdout.flush()
```


## Use Cases

### 1. Limiting File Uploads

Prevent users from uploading large files to specific endpoints:

```javascript
// JavaScript engine
const uploadPaths = ['/upload', '/api/files'];
uploadPaths.some(path => r.path.startsWith(path))
? {allow: {max_tx_bytes: 10485760}} // 10MB limit
: true
```

### 2. API Cost Control

Limit request sizes to metered APIs to prevent unexpected costs:

```javascript
// JavaScript engine
r.host === 'api.expensive-service.com'
? {allow: {max_tx_bytes: 1024}} // 1KB limit for expensive API
: true
```

### 3. Data Exfiltration Prevention

Prevent large data uploads that might indicate data exfiltration:

```javascript
// JavaScript engine
const externalHosts = ['pastebin.com', 'transfer.sh', 'file.io'];
externalHosts.some(host => r.host.includes(host))
? {allow: {max_tx_bytes: 4096}} // 4KB limit for paste sites
: true
```

## Limitations

- **Shell scripts**: The `max_tx_bytes` feature is not available when using shell script rules (`--shell`)
- **HTTP wire format**: The byte count is based on HTTP wire format, not just the body size
- **Partial uploads**: When truncating (no Content-Length), the upstream server receives incomplete data which may cause application errors

## See Also

- [JavaScript Engine](../guide/rule-engines/javascript.md)
- [Line Processor Engine](../guide/rule-engines/line-processor.md)
- [Configuration](../guide/configuration.md)
22 changes: 22 additions & 0 deletions docs/custom.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/* Minimal CSS to enable multi-line code in tables */
/* Let mdbook theme handle all other styling */

table td pre {
margin: 0;
}

table td pre code {
display: block;
}

/* Make <br> tags work as line breaks in code */
table td pre code br {
display: block;
content: "";
}

/* Keep tables left-aligned - override general.css margin: 0 auto */
.table-wrapper table {
margin-left: 0;
margin-right: 0;
}
11 changes: 6 additions & 5 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,11 @@ allowedHosts.includes(r.host);
httpjail --js-file rules.js -- command
```

## Response Types
## Response Format

Your JavaScript can return:
{{#include ../../includes/response-format-table.md}}

- **Boolean**: `true` to allow, `false` to deny
- **Object with message**: `{allow: false, deny_message: "Custom error"}`
- **Just a message**: `{deny_message: "Blocked"}` (implies deny)
**Examples:**

```javascript
// Simple boolean
Expand All@@ -59,6 +57,9 @@ false // Deny

// Conditional with message
r.host === 'facebook.com' ? {deny_message: 'Social media blocked'} : true

// Limit request upload size to 1KB (headers + body)
({allow: {max_tx_bytes: 1024}})
```

## Common Patterns
Expand Down
14 changes: 10 additions & 4 deletions docs/guide/rule-engines/line-processor.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,11 +27,12 @@ Each request is sent as a single JSON line:

### Response Format

Your processor must respond with one line per request:
Your processor must respond with one line per request.

- **Boolean strings**: `"true"` (allow) or `"false"` (deny)
- **JSON object**: `{"allow": false, "deny_message": "Blocked by policy"}`
- **JSON with message only**: `{"deny_message": "Blocked"}` (implies deny)
{{#include ../../includes/response-format-table.md}}

**Additional:**
- **Boolean strings**: `"true"` (allow) or `"false"` (deny) - same as boolean
- **Any other text**: Treated as deny with that text as the message (e.g., `"Access denied"` becomes a deny with message "Access denied")

## Command Line Usage
Expand All@@ -56,12 +57,17 @@ httpjail --proc "./filter.sh --strict" -- your-command
import sys, json

allowed_hosts = {'github.com', 'api.github.com'}
upload_hosts = {'uploads.example.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in allowed_hosts:
print("true")
elif req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
else:
# Can return JSON for custom messages
response = {"allow": False, "deny_message": f"{req['host']} not allowed"}
Expand Down
9 changes: 9 additions & 0 deletions docs/includes/response-format-table.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
| Response Format | Meaning |
|----------------|---------|
| `true` | Allow the request |
| `false` | Deny the request |
| `{allow: true}` | Allow (object form) |
| `{allow: false}` | Deny (object form) |
| <pre><code>{<br> allow: false,<br> deny_message: "Access denied"<br>}</code></pre> | Deny with custom message |
| `{deny_message: "Blocked"}` | Deny (message implies deny) |
| <pre><code>{<br> allow: {<br> max_tx_bytes: 1024<br> }<br>}</code></pre> | Allow with [request body limiting](../../advanced/request-body-limiting.md) |
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat: add support for `max_tx_bytes` as limited allow policy by ammario · Pull Request #75 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 4 additions & 20 deletions .github/workflows/docs.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,27 +23,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: 'latest'

- name: Install mdbook-mermaid
run: |
VERSION=$(curl -s https://api.github.com/repos/badboy/mdbook-mermaid/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -sSL "https://github.com/badboy/mdbook-mermaid/releases/download/${VERSION}/mdbook-mermaid-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" | tar -xz
sudo mv mdbook-mermaid /usr/local/bin/
mdbook-mermaid install .

- name: Install mdbook-linkcheck
run: |
curl -sSL https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/latest/download/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -o linkcheck.zip
unzip -q -o linkcheck.zip
chmod +x mdbook-linkcheck
sudo mv mdbook-linkcheck /usr/local/bin/

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Build documentation
run: mdbook build
run: ./scripts/mdbook.sh build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
Expand Down
2 changes: 1 addition & 1 deletion book.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ site-url = "https://coder.github.io/httpjail/"
cname = ""
mathjax-support = false
copy-fonts = true
additional-css = []
additional-css = ["docs/custom.css"]
additional-js = ["mermaid.min.js", "mermaid-init.js"]
no-section-label = false
fold.enable = true
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@

# Advanced

- [Request Body Limiting](./advanced/request-body-limiting.md)
- [TLS Interception](./advanced/tls-interception.md)
- [DNS Exfiltration](./advanced/dns-exfiltration.md)
- [Server Mode](./advanced/server-mode.md)
Expand Down
148 changes: 148 additions & 0 deletions docs/advanced/request-body-limiting.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
# Request Body Limiting

The `max_tx_bytes` feature allows you to limit the total size of HTTP requests sent to upstream servers.

This is primarily designed for mitigating code exfiltration attacks through covert channels.

## Size Calculation

The `max_tx_bytes` limit applies to **complete** HTTP requests, including:

1. **Request line**: `METHOD /path HTTP/1.1\r\n`
2. **Headers**: Each header as `Name: Value\r\n`
3. **Header separator**: Final `\r\n` between headers and body
4. **Body**: Request body bytes

## Response Format

To enable request body limiting, return an object with `max_tx_bytes` in your rule response:

```javascript
// JavaScript engine
{allow: {max_tx_bytes: 1024}} // Limit to 1KB total request size
```

```json
// Line processor engine
{"allow": {"max_tx_bytes": 1024}}
```

> **Note**: The `max_tx_bytes` feature is only available in the JavaScript (`--js`) and Line Processor (`--proc`) engines, not in Shell scripts.

## Behavior

The limiting behavior depends on whether the request includes a `Content-Length` header:

### With Content-Length Header

When the request includes a `Content-Length` header (most standard HTTP clients):

1. **Early Detection**: httpjail calculates the total request size
2. **Immediate Rejection**: If it exceeds `max_tx_bytes`, the client receives a `413 Payload Too Large` error immediately
3. **No Upstream Contact**: The upstream server is never contacted, preventing unnecessary load
4. **Clear Feedback**: The error message indicates the actual size and limit

**Example error response:**
```
HTTP/1.1 413 Payload Too Large
Content-Type: text/plain

Request body size (5000 bytes) exceeds maximum allowed (1024 bytes)
```

### Without Content-Length Header

When the request uses chunked encoding or doesn't include `Content-Length`:

1. **Stream Truncation**: The request body is truncated at the limit during streaming
2. **Upstream Receives Partial**: The upstream server receives exactly `max_tx_bytes` total bytes (url + headers + truncated body)
3. **Connection Closes**: The connection terminates after reaching the limit

## Examples

### JavaScript Engine - Upload Endpoint Limiting

```javascript
// Limit upload endpoints to 1KB total request size
const uploadHosts = ['uploads.example.com', 'upload.github.com'];

uploadHosts.includes(r.host)
? {allow: {max_tx_bytes: 1024}}
: r.host.endsWith('.example.com')
```

### Line Processor Engine - Python Example

```python
#!/usr/bin/env python3
import sys, json

upload_hosts = {'uploads.example.com', 'data.api.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
# Returns 413 error if Content-Length exceeds limit
# Truncates body if no Content-Length header
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
elif req['host'].endswith('.example.com'):
print("true")
else:
print("false")
except:
print("false")
sys.stdout.flush()
```


## Use Cases

### 1. Limiting File Uploads

Prevent users from uploading large files to specific endpoints:

```javascript
// JavaScript engine
const uploadPaths = ['/upload', '/api/files'];
uploadPaths.some(path => r.path.startsWith(path))
? {allow: {max_tx_bytes: 10485760}} // 10MB limit
: true
```

### 2. API Cost Control

Limit request sizes to metered APIs to prevent unexpected costs:

```javascript
// JavaScript engine
r.host === 'api.expensive-service.com'
? {allow: {max_tx_bytes: 1024}} // 1KB limit for expensive API
: true
```

### 3. Data Exfiltration Prevention

Prevent large data uploads that might indicate data exfiltration:

```javascript
// JavaScript engine
const externalHosts = ['pastebin.com', 'transfer.sh', 'file.io'];
externalHosts.some(host => r.host.includes(host))
? {allow: {max_tx_bytes: 4096}} // 4KB limit for paste sites
: true
```

## Limitations

- **Shell scripts**: The `max_tx_bytes` feature is not available when using shell script rules (`--shell`)
- **HTTP wire format**: The byte count is based on HTTP wire format, not just the body size
- **Partial uploads**: When truncating (no Content-Length), the upstream server receives incomplete data which may cause application errors

## See Also

- [JavaScript Engine](../guide/rule-engines/javascript.md)
- [Line Processor Engine](../guide/rule-engines/line-processor.md)
- [Configuration](../guide/configuration.md)
22 changes: 22 additions & 0 deletions docs/custom.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/* Minimal CSS to enable multi-line code in tables */
/* Let mdbook theme handle all other styling */

table td pre {
margin: 0;
}

table td pre code {
display: block;
}

/* Make <br> tags work as line breaks in code */
table td pre code br {
display: block;
content: "";
}

/* Keep tables left-aligned - override general.css margin: 0 auto */
.table-wrapper table {
margin-left: 0;
margin-right: 0;
}
11 changes: 6 additions & 5 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,11 @@ allowedHosts.includes(r.host);
httpjail --js-file rules.js -- command
```

## Response Types
## Response Format

Your JavaScript can return:
{{#include ../../includes/response-format-table.md}}

- **Boolean**: `true` to allow, `false` to deny
- **Object with message**: `{allow: false, deny_message: "Custom error"}`
- **Just a message**: `{deny_message: "Blocked"}` (implies deny)
**Examples:**

```javascript
// Simple boolean
Expand All@@ -59,6 +57,9 @@ false // Deny

// Conditional with message
r.host === 'facebook.com' ? {deny_message: 'Social media blocked'} : true

// Limit request upload size to 1KB (headers + body)
({allow: {max_tx_bytes: 1024}})
```

## Common Patterns
Expand Down
14 changes: 10 additions & 4 deletions docs/guide/rule-engines/line-processor.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,11 +27,12 @@ Each request is sent as a single JSON line:

### Response Format

Your processor must respond with one line per request:
Your processor must respond with one line per request.

- **Boolean strings**: `"true"` (allow) or `"false"` (deny)
- **JSON object**: `{"allow": false, "deny_message": "Blocked by policy"}`
- **JSON with message only**: `{"deny_message": "Blocked"}` (implies deny)
{{#include ../../includes/response-format-table.md}}

**Additional:**
- **Boolean strings**: `"true"` (allow) or `"false"` (deny) - same as boolean
- **Any other text**: Treated as deny with that text as the message (e.g., `"Access denied"` becomes a deny with message "Access denied")

## Command Line Usage
Expand All@@ -56,12 +57,17 @@ httpjail --proc "./filter.sh --strict" -- your-command
import sys, json

allowed_hosts = {'github.com', 'api.github.com'}
upload_hosts = {'uploads.example.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in allowed_hosts:
print("true")
elif req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
else:
# Can return JSON for custom messages
response = {"allow": False, "deny_message": f"{req['host']} not allowed"}
Expand Down
9 changes: 9 additions & 0 deletions docs/includes/response-format-table.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
| Response Format | Meaning |
|----------------|---------|
| `true` | Allow the request |
| `false` | Deny the request |
| `{allow: true}` | Allow (object form) |
| `{allow: false}` | Deny (object form) |
| <pre><code>{<br> allow: false,<br> deny_message: "Access denied"<br>}</code></pre> | Deny with custom message |
| `{deny_message: "Blocked"}` | Deny (message implies deny) |
| <pre><code>{<br> allow: {<br> max_tx_bytes: 1024<br> }<br>}</code></pre> | Allow with [request body limiting](../../advanced/request-body-limiting.md) |
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: add support for `max_tx_bytes` as limited allow policy by ammario · Pull Request #75 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 4 additions & 20 deletions .github/workflows/docs.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,27 +23,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: 'latest'

- name: Install mdbook-mermaid
run: |
VERSION=$(curl -s https://api.github.com/repos/badboy/mdbook-mermaid/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -sSL "https://github.com/badboy/mdbook-mermaid/releases/download/${VERSION}/mdbook-mermaid-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" | tar -xz
sudo mv mdbook-mermaid /usr/local/bin/
mdbook-mermaid install .

- name: Install mdbook-linkcheck
run: |
curl -sSL https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/latest/download/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -o linkcheck.zip
unzip -q -o linkcheck.zip
chmod +x mdbook-linkcheck
sudo mv mdbook-linkcheck /usr/local/bin/

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Build documentation
run: mdbook build
run: ./scripts/mdbook.sh build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
Expand Down
2 changes: 1 addition & 1 deletion book.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ site-url = "https://coder.github.io/httpjail/"
cname = ""
mathjax-support = false
copy-fonts = true
additional-css = []
additional-css = ["docs/custom.css"]
additional-js = ["mermaid.min.js", "mermaid-init.js"]
no-section-label = false
fold.enable = true
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@

# Advanced

- [Request Body Limiting](./advanced/request-body-limiting.md)
- [TLS Interception](./advanced/tls-interception.md)
- [DNS Exfiltration](./advanced/dns-exfiltration.md)
- [Server Mode](./advanced/server-mode.md)
Expand Down
148 changes: 148 additions & 0 deletions docs/advanced/request-body-limiting.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
# Request Body Limiting

The `max_tx_bytes` feature allows you to limit the total size of HTTP requests sent to upstream servers.

This is primarily designed for mitigating code exfiltration attacks through covert channels.

## Size Calculation

The `max_tx_bytes` limit applies to **complete** HTTP requests, including:

1. **Request line**: `METHOD /path HTTP/1.1\r\n`
2. **Headers**: Each header as `Name: Value\r\n`
3. **Header separator**: Final `\r\n` between headers and body
4. **Body**: Request body bytes

## Response Format

To enable request body limiting, return an object with `max_tx_bytes` in your rule response:

```javascript
// JavaScript engine
{allow: {max_tx_bytes: 1024}} // Limit to 1KB total request size
```

```json
// Line processor engine
{"allow": {"max_tx_bytes": 1024}}
```

> **Note**: The `max_tx_bytes` feature is only available in the JavaScript (`--js`) and Line Processor (`--proc`) engines, not in Shell scripts.

## Behavior

The limiting behavior depends on whether the request includes a `Content-Length` header:

### With Content-Length Header

When the request includes a `Content-Length` header (most standard HTTP clients):

1. **Early Detection**: httpjail calculates the total request size
2. **Immediate Rejection**: If it exceeds `max_tx_bytes`, the client receives a `413 Payload Too Large` error immediately
3. **No Upstream Contact**: The upstream server is never contacted, preventing unnecessary load
4. **Clear Feedback**: The error message indicates the actual size and limit

**Example error response:**
```
HTTP/1.1 413 Payload Too Large
Content-Type: text/plain

Request body size (5000 bytes) exceeds maximum allowed (1024 bytes)
```

### Without Content-Length Header

When the request uses chunked encoding or doesn't include `Content-Length`:

1. **Stream Truncation**: The request body is truncated at the limit during streaming
2. **Upstream Receives Partial**: The upstream server receives exactly `max_tx_bytes` total bytes (url + headers + truncated body)
3. **Connection Closes**: The connection terminates after reaching the limit

## Examples

### JavaScript Engine - Upload Endpoint Limiting

```javascript
// Limit upload endpoints to 1KB total request size
const uploadHosts = ['uploads.example.com', 'upload.github.com'];

uploadHosts.includes(r.host)
? {allow: {max_tx_bytes: 1024}}
: r.host.endsWith('.example.com')
```

### Line Processor Engine - Python Example

```python
#!/usr/bin/env python3
import sys, json

upload_hosts = {'uploads.example.com', 'data.api.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
# Returns 413 error if Content-Length exceeds limit
# Truncates body if no Content-Length header
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
elif req['host'].endswith('.example.com'):
print("true")
else:
print("false")
except:
print("false")
sys.stdout.flush()
```


## Use Cases

### 1. Limiting File Uploads

Prevent users from uploading large files to specific endpoints:

```javascript
// JavaScript engine
const uploadPaths = ['/upload', '/api/files'];
uploadPaths.some(path => r.path.startsWith(path))
? {allow: {max_tx_bytes: 10485760}} // 10MB limit
: true
```

### 2. API Cost Control

Limit request sizes to metered APIs to prevent unexpected costs:

```javascript
// JavaScript engine
r.host === 'api.expensive-service.com'
? {allow: {max_tx_bytes: 1024}} // 1KB limit for expensive API
: true
```

### 3. Data Exfiltration Prevention

Prevent large data uploads that might indicate data exfiltration:

```javascript
// JavaScript engine
const externalHosts = ['pastebin.com', 'transfer.sh', 'file.io'];
externalHosts.some(host => r.host.includes(host))
? {allow: {max_tx_bytes: 4096}} // 4KB limit for paste sites
: true
```

## Limitations

- **Shell scripts**: The `max_tx_bytes` feature is not available when using shell script rules (`--shell`)
- **HTTP wire format**: The byte count is based on HTTP wire format, not just the body size
- **Partial uploads**: When truncating (no Content-Length), the upstream server receives incomplete data which may cause application errors

## See Also

- [JavaScript Engine](../guide/rule-engines/javascript.md)
- [Line Processor Engine](../guide/rule-engines/line-processor.md)
- [Configuration](../guide/configuration.md)
22 changes: 22 additions & 0 deletions docs/custom.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/* Minimal CSS to enable multi-line code in tables */
/* Let mdbook theme handle all other styling */

table td pre {
margin: 0;
}

table td pre code {
display: block;
}

/* Make <br> tags work as line breaks in code */
table td pre code br {
display: block;
content: "";
}

/* Keep tables left-aligned - override general.css margin: 0 auto */
.table-wrapper table {
margin-left: 0;
margin-right: 0;
}
11 changes: 6 additions & 5 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,11 @@ allowedHosts.includes(r.host);
httpjail --js-file rules.js -- command
```

## Response Types
## Response Format

Your JavaScript can return:
{{#include ../../includes/response-format-table.md}}

- **Boolean**: `true` to allow, `false` to deny
- **Object with message**: `{allow: false, deny_message: "Custom error"}`
- **Just a message**: `{deny_message: "Blocked"}` (implies deny)
**Examples:**

```javascript
// Simple boolean
Expand All@@ -59,6 +57,9 @@ false // Deny

// Conditional with message
r.host === 'facebook.com' ? {deny_message: 'Social media blocked'} : true

// Limit request upload size to 1KB (headers + body)
({allow: {max_tx_bytes: 1024}})
```

## Common Patterns
Expand Down
14 changes: 10 additions & 4 deletions docs/guide/rule-engines/line-processor.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,11 +27,12 @@ Each request is sent as a single JSON line:

### Response Format

Your processor must respond with one line per request:
Your processor must respond with one line per request.

- **Boolean strings**: `"true"` (allow) or `"false"` (deny)
- **JSON object**: `{"allow": false, "deny_message": "Blocked by policy"}`
- **JSON with message only**: `{"deny_message": "Blocked"}` (implies deny)
{{#include ../../includes/response-format-table.md}}

**Additional:**
- **Boolean strings**: `"true"` (allow) or `"false"` (deny) - same as boolean
- **Any other text**: Treated as deny with that text as the message (e.g., `"Access denied"` becomes a deny with message "Access denied")

## Command Line Usage
Expand All@@ -56,12 +57,17 @@ httpjail --proc "./filter.sh --strict" -- your-command
import sys, json

allowed_hosts = {'github.com', 'api.github.com'}
upload_hosts = {'uploads.example.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in allowed_hosts:
print("true")
elif req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
else:
# Can return JSON for custom messages
response = {"allow": False, "deny_message": f"{req['host']} not allowed"}
Expand Down
9 changes: 9 additions & 0 deletions docs/includes/response-format-table.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
| Response Format | Meaning |
|----------------|---------|
| `true` | Allow the request |
| `false` | Deny the request |
| `{allow: true}` | Allow (object form) |
| `{allow: false}` | Deny (object form) |
| <pre><code>{<br> allow: false,<br> deny_message: "Access denied"<br>}</code></pre> | Deny with custom message |
| `{deny_message: "Blocked"}` | Deny (message implies deny) |
| <pre><code>{<br> allow: {<br> max_tx_bytes: 1024<br> }<br>}</code></pre> | Allow with [request body limiting](../../advanced/request-body-limiting.md) |
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: add support for `max_tx_bytes` as limited allow policy by ammario · Pull Request #75 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 4 additions & 20 deletions .github/workflows/docs.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,27 +23,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: 'latest'

- name: Install mdbook-mermaid
run: |
VERSION=$(curl -s https://api.github.com/repos/badboy/mdbook-mermaid/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -sSL "https://github.com/badboy/mdbook-mermaid/releases/download/${VERSION}/mdbook-mermaid-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" | tar -xz
sudo mv mdbook-mermaid /usr/local/bin/
mdbook-mermaid install .

- name: Install mdbook-linkcheck
run: |
curl -sSL https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/latest/download/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -o linkcheck.zip
unzip -q -o linkcheck.zip
chmod +x mdbook-linkcheck
sudo mv mdbook-linkcheck /usr/local/bin/

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Build documentation
run: mdbook build
run: ./scripts/mdbook.sh build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
Expand Down
2 changes: 1 addition & 1 deletion book.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ site-url = "https://coder.github.io/httpjail/"
cname = ""
mathjax-support = false
copy-fonts = true
additional-css = []
additional-css = ["docs/custom.css"]
additional-js = ["mermaid.min.js", "mermaid-init.js"]
no-section-label = false
fold.enable = true
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@

# Advanced

- [Request Body Limiting](./advanced/request-body-limiting.md)
- [TLS Interception](./advanced/tls-interception.md)
- [DNS Exfiltration](./advanced/dns-exfiltration.md)
- [Server Mode](./advanced/server-mode.md)
Expand Down
148 changes: 148 additions & 0 deletions docs/advanced/request-body-limiting.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
# Request Body Limiting

The `max_tx_bytes` feature allows you to limit the total size of HTTP requests sent to upstream servers.

This is primarily designed for mitigating code exfiltration attacks through covert channels.

## Size Calculation

The `max_tx_bytes` limit applies to **complete** HTTP requests, including:

1. **Request line**: `METHOD /path HTTP/1.1\r\n`
2. **Headers**: Each header as `Name: Value\r\n`
3. **Header separator**: Final `\r\n` between headers and body
4. **Body**: Request body bytes

## Response Format

To enable request body limiting, return an object with `max_tx_bytes` in your rule response:

```javascript
// JavaScript engine
{allow: {max_tx_bytes: 1024}} // Limit to 1KB total request size
```

```json
// Line processor engine
{"allow": {"max_tx_bytes": 1024}}
```

> **Note**: The `max_tx_bytes` feature is only available in the JavaScript (`--js`) and Line Processor (`--proc`) engines, not in Shell scripts.

## Behavior

The limiting behavior depends on whether the request includes a `Content-Length` header:

### With Content-Length Header

When the request includes a `Content-Length` header (most standard HTTP clients):

1. **Early Detection**: httpjail calculates the total request size
2. **Immediate Rejection**: If it exceeds `max_tx_bytes`, the client receives a `413 Payload Too Large` error immediately
3. **No Upstream Contact**: The upstream server is never contacted, preventing unnecessary load
4. **Clear Feedback**: The error message indicates the actual size and limit

**Example error response:**
```
HTTP/1.1 413 Payload Too Large
Content-Type: text/plain

Request body size (5000 bytes) exceeds maximum allowed (1024 bytes)
```

### Without Content-Length Header

When the request uses chunked encoding or doesn't include `Content-Length`:

1. **Stream Truncation**: The request body is truncated at the limit during streaming
2. **Upstream Receives Partial**: The upstream server receives exactly `max_tx_bytes` total bytes (url + headers + truncated body)
3. **Connection Closes**: The connection terminates after reaching the limit

## Examples

### JavaScript Engine - Upload Endpoint Limiting

```javascript
// Limit upload endpoints to 1KB total request size
const uploadHosts = ['uploads.example.com', 'upload.github.com'];

uploadHosts.includes(r.host)
? {allow: {max_tx_bytes: 1024}}
: r.host.endsWith('.example.com')
```

### Line Processor Engine - Python Example

```python
#!/usr/bin/env python3
import sys, json

upload_hosts = {'uploads.example.com', 'data.api.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
# Returns 413 error if Content-Length exceeds limit
# Truncates body if no Content-Length header
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
elif req['host'].endswith('.example.com'):
print("true")
else:
print("false")
except:
print("false")
sys.stdout.flush()
```


## Use Cases

### 1. Limiting File Uploads

Prevent users from uploading large files to specific endpoints:

```javascript
// JavaScript engine
const uploadPaths = ['/upload', '/api/files'];
uploadPaths.some(path => r.path.startsWith(path))
? {allow: {max_tx_bytes: 10485760}} // 10MB limit
: true
```

### 2. API Cost Control

Limit request sizes to metered APIs to prevent unexpected costs:

```javascript
// JavaScript engine
r.host === 'api.expensive-service.com'
? {allow: {max_tx_bytes: 1024}} // 1KB limit for expensive API
: true
```

### 3. Data Exfiltration Prevention

Prevent large data uploads that might indicate data exfiltration:

```javascript
// JavaScript engine
const externalHosts = ['pastebin.com', 'transfer.sh', 'file.io'];
externalHosts.some(host => r.host.includes(host))
? {allow: {max_tx_bytes: 4096}} // 4KB limit for paste sites
: true
```

## Limitations

- **Shell scripts**: The `max_tx_bytes` feature is not available when using shell script rules (`--shell`)
- **HTTP wire format**: The byte count is based on HTTP wire format, not just the body size
- **Partial uploads**: When truncating (no Content-Length), the upstream server receives incomplete data which may cause application errors

## See Also

- [JavaScript Engine](../guide/rule-engines/javascript.md)
- [Line Processor Engine](../guide/rule-engines/line-processor.md)
- [Configuration](../guide/configuration.md)
22 changes: 22 additions & 0 deletions docs/custom.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/* Minimal CSS to enable multi-line code in tables */
/* Let mdbook theme handle all other styling */

table td pre {
margin: 0;
}

table td pre code {
display: block;
}

/* Make <br> tags work as line breaks in code */
table td pre code br {
display: block;
content: "";
}

/* Keep tables left-aligned - override general.css margin: 0 auto */
.table-wrapper table {
margin-left: 0;
margin-right: 0;
}
11 changes: 6 additions & 5 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,11 @@ allowedHosts.includes(r.host);
httpjail --js-file rules.js -- command
```

## Response Types
## Response Format

Your JavaScript can return:
{{#include ../../includes/response-format-table.md}}

- **Boolean**: `true` to allow, `false` to deny
- **Object with message**: `{allow: false, deny_message: "Custom error"}`
- **Just a message**: `{deny_message: "Blocked"}` (implies deny)
**Examples:**

```javascript
// Simple boolean
Expand All@@ -59,6 +57,9 @@ false // Deny

// Conditional with message
r.host === 'facebook.com' ? {deny_message: 'Social media blocked'} : true

// Limit request upload size to 1KB (headers + body)
({allow: {max_tx_bytes: 1024}})
```

## Common Patterns
Expand Down
14 changes: 10 additions & 4 deletions docs/guide/rule-engines/line-processor.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,11 +27,12 @@ Each request is sent as a single JSON line:

### Response Format

Your processor must respond with one line per request:
Your processor must respond with one line per request.

- **Boolean strings**: `"true"` (allow) or `"false"` (deny)
- **JSON object**: `{"allow": false, "deny_message": "Blocked by policy"}`
- **JSON with message only**: `{"deny_message": "Blocked"}` (implies deny)
{{#include ../../includes/response-format-table.md}}

**Additional:**
- **Boolean strings**: `"true"` (allow) or `"false"` (deny) - same as boolean
- **Any other text**: Treated as deny with that text as the message (e.g., `"Access denied"` becomes a deny with message "Access denied")

## Command Line Usage
Expand All@@ -56,12 +57,17 @@ httpjail --proc "./filter.sh --strict" -- your-command
import sys, json

allowed_hosts = {'github.com', 'api.github.com'}
upload_hosts = {'uploads.example.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in allowed_hosts:
print("true")
elif req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
else:
# Can return JSON for custom messages
response = {"allow": False, "deny_message": f"{req['host']} not allowed"}
Expand Down
9 changes: 9 additions & 0 deletions docs/includes/response-format-table.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
| Response Format | Meaning |
|----------------|---------|
| `true` | Allow the request |
| `false` | Deny the request |
| `{allow: true}` | Allow (object form) |
| `{allow: false}` | Deny (object form) |
| <pre><code>{<br> allow: false,<br> deny_message: "Access denied"<br>}</code></pre> | Deny with custom message |
| `{deny_message: "Blocked"}` | Deny (message implies deny) |
| <pre><code>{<br> allow: {<br> max_tx_bytes: 1024<br> }<br>}</code></pre> | Allow with [request body limiting](../../advanced/request-body-limiting.md) |
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat: add support for `max_tx_bytes` as limited allow policy by ammario · Pull Request #75 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 4 additions & 20 deletions .github/workflows/docs.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,27 +23,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: 'latest'

- name: Install mdbook-mermaid
run: |
VERSION=$(curl -s https://api.github.com/repos/badboy/mdbook-mermaid/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -sSL "https://github.com/badboy/mdbook-mermaid/releases/download/${VERSION}/mdbook-mermaid-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" | tar -xz
sudo mv mdbook-mermaid /usr/local/bin/
mdbook-mermaid install .

- name: Install mdbook-linkcheck
run: |
curl -sSL https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/latest/download/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -o linkcheck.zip
unzip -q -o linkcheck.zip
chmod +x mdbook-linkcheck
sudo mv mdbook-linkcheck /usr/local/bin/

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Build documentation
run: mdbook build
run: ./scripts/mdbook.sh build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
Expand Down
2 changes: 1 addition & 1 deletion book.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ site-url = "https://coder.github.io/httpjail/"
cname = ""
mathjax-support = false
copy-fonts = true
additional-css = []
additional-css = ["docs/custom.css"]
additional-js = ["mermaid.min.js", "mermaid-init.js"]
no-section-label = false
fold.enable = true
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@

# Advanced

- [Request Body Limiting](./advanced/request-body-limiting.md)
- [TLS Interception](./advanced/tls-interception.md)
- [DNS Exfiltration](./advanced/dns-exfiltration.md)
- [Server Mode](./advanced/server-mode.md)
Expand Down
148 changes: 148 additions & 0 deletions docs/advanced/request-body-limiting.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
# Request Body Limiting

The `max_tx_bytes` feature allows you to limit the total size of HTTP requests sent to upstream servers.

This is primarily designed for mitigating code exfiltration attacks through covert channels.

## Size Calculation

The `max_tx_bytes` limit applies to **complete** HTTP requests, including:

1. **Request line**: `METHOD /path HTTP/1.1\r\n`
2. **Headers**: Each header as `Name: Value\r\n`
3. **Header separator**: Final `\r\n` between headers and body
4. **Body**: Request body bytes

## Response Format

To enable request body limiting, return an object with `max_tx_bytes` in your rule response:

```javascript
// JavaScript engine
{allow: {max_tx_bytes: 1024}} // Limit to 1KB total request size
```

```json
// Line processor engine
{"allow": {"max_tx_bytes": 1024}}
```

> **Note**: The `max_tx_bytes` feature is only available in the JavaScript (`--js`) and Line Processor (`--proc`) engines, not in Shell scripts.

## Behavior

The limiting behavior depends on whether the request includes a `Content-Length` header:

### With Content-Length Header

When the request includes a `Content-Length` header (most standard HTTP clients):

1. **Early Detection**: httpjail calculates the total request size
2. **Immediate Rejection**: If it exceeds `max_tx_bytes`, the client receives a `413 Payload Too Large` error immediately
3. **No Upstream Contact**: The upstream server is never contacted, preventing unnecessary load
4. **Clear Feedback**: The error message indicates the actual size and limit

**Example error response:**
```
HTTP/1.1 413 Payload Too Large
Content-Type: text/plain

Request body size (5000 bytes) exceeds maximum allowed (1024 bytes)
```

### Without Content-Length Header

When the request uses chunked encoding or doesn't include `Content-Length`:

1. **Stream Truncation**: The request body is truncated at the limit during streaming
2. **Upstream Receives Partial**: The upstream server receives exactly `max_tx_bytes` total bytes (url + headers + truncated body)
3. **Connection Closes**: The connection terminates after reaching the limit

## Examples

### JavaScript Engine - Upload Endpoint Limiting

```javascript
// Limit upload endpoints to 1KB total request size
const uploadHosts = ['uploads.example.com', 'upload.github.com'];

uploadHosts.includes(r.host)
? {allow: {max_tx_bytes: 1024}}
: r.host.endsWith('.example.com')
```

### Line Processor Engine - Python Example

```python
#!/usr/bin/env python3
import sys, json

upload_hosts = {'uploads.example.com', 'data.api.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
# Returns 413 error if Content-Length exceeds limit
# Truncates body if no Content-Length header
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
elif req['host'].endswith('.example.com'):
print("true")
else:
print("false")
except:
print("false")
sys.stdout.flush()
```


## Use Cases

### 1. Limiting File Uploads

Prevent users from uploading large files to specific endpoints:

```javascript
// JavaScript engine
const uploadPaths = ['/upload', '/api/files'];
uploadPaths.some(path => r.path.startsWith(path))
? {allow: {max_tx_bytes: 10485760}} // 10MB limit
: true
```

### 2. API Cost Control

Limit request sizes to metered APIs to prevent unexpected costs:

```javascript
// JavaScript engine
r.host === 'api.expensive-service.com'
? {allow: {max_tx_bytes: 1024}} // 1KB limit for expensive API
: true
```

### 3. Data Exfiltration Prevention

Prevent large data uploads that might indicate data exfiltration:

```javascript
// JavaScript engine
const externalHosts = ['pastebin.com', 'transfer.sh', 'file.io'];
externalHosts.some(host => r.host.includes(host))
? {allow: {max_tx_bytes: 4096}} // 4KB limit for paste sites
: true
```

## Limitations

- **Shell scripts**: The `max_tx_bytes` feature is not available when using shell script rules (`--shell`)
- **HTTP wire format**: The byte count is based on HTTP wire format, not just the body size
- **Partial uploads**: When truncating (no Content-Length), the upstream server receives incomplete data which may cause application errors

## See Also

- [JavaScript Engine](../guide/rule-engines/javascript.md)
- [Line Processor Engine](../guide/rule-engines/line-processor.md)
- [Configuration](../guide/configuration.md)
22 changes: 22 additions & 0 deletions docs/custom.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/* Minimal CSS to enable multi-line code in tables */
/* Let mdbook theme handle all other styling */

table td pre {
margin: 0;
}

table td pre code {
display: block;
}

/* Make <br> tags work as line breaks in code */
table td pre code br {
display: block;
content: "";
}

/* Keep tables left-aligned - override general.css margin: 0 auto */
.table-wrapper table {
margin-left: 0;
margin-right: 0;
}
11 changes: 6 additions & 5 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,11 @@ allowedHosts.includes(r.host);
httpjail --js-file rules.js -- command
```

## Response Types
## Response Format

Your JavaScript can return:
{{#include ../../includes/response-format-table.md}}

- **Boolean**: `true` to allow, `false` to deny
- **Object with message**: `{allow: false, deny_message: "Custom error"}`
- **Just a message**: `{deny_message: "Blocked"}` (implies deny)
**Examples:**

```javascript
// Simple boolean
Expand All@@ -59,6 +57,9 @@ false // Deny

// Conditional with message
r.host === 'facebook.com' ? {deny_message: 'Social media blocked'} : true

// Limit request upload size to 1KB (headers + body)
({allow: {max_tx_bytes: 1024}})
```

## Common Patterns
Expand Down
14 changes: 10 additions & 4 deletions docs/guide/rule-engines/line-processor.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,11 +27,12 @@ Each request is sent as a single JSON line:

### Response Format

Your processor must respond with one line per request:
Your processor must respond with one line per request.

- **Boolean strings**: `"true"` (allow) or `"false"` (deny)
- **JSON object**: `{"allow": false, "deny_message": "Blocked by policy"}`
- **JSON with message only**: `{"deny_message": "Blocked"}` (implies deny)
{{#include ../../includes/response-format-table.md}}

**Additional:**
- **Boolean strings**: `"true"` (allow) or `"false"` (deny) - same as boolean
- **Any other text**: Treated as deny with that text as the message (e.g., `"Access denied"` becomes a deny with message "Access denied")

## Command Line Usage
Expand All@@ -56,12 +57,17 @@ httpjail --proc "./filter.sh --strict" -- your-command
import sys, json

allowed_hosts = {'github.com', 'api.github.com'}
upload_hosts = {'uploads.example.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in allowed_hosts:
print("true")
elif req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
else:
# Can return JSON for custom messages
response = {"allow": False, "deny_message": f"{req['host']} not allowed"}
Expand Down
9 changes: 9 additions & 0 deletions docs/includes/response-format-table.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
| Response Format | Meaning |
|----------------|---------|
| `true` | Allow the request |
| `false` | Deny the request |
| `{allow: true}` | Allow (object form) |
| `{allow: false}` | Deny (object form) |
| <pre><code>{<br> allow: false,<br> deny_message: "Access denied"<br>}</code></pre> | Deny with custom message |
| `{deny_message: "Blocked"}` | Deny (message implies deny) |
| <pre><code>{<br> allow: {<br> max_tx_bytes: 1024<br> }<br>}</code></pre> | Allow with [request body limiting](../../advanced/request-body-limiting.md) |
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: add support for `max_tx_bytes` as limited allow policy by ammario · Pull Request #75 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 4 additions & 20 deletions .github/workflows/docs.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,27 +23,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: 'latest'

- name: Install mdbook-mermaid
run: |
VERSION=$(curl -s https://api.github.com/repos/badboy/mdbook-mermaid/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -sSL "https://github.com/badboy/mdbook-mermaid/releases/download/${VERSION}/mdbook-mermaid-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" | tar -xz
sudo mv mdbook-mermaid /usr/local/bin/
mdbook-mermaid install .

- name: Install mdbook-linkcheck
run: |
curl -sSL https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/latest/download/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -o linkcheck.zip
unzip -q -o linkcheck.zip
chmod +x mdbook-linkcheck
sudo mv mdbook-linkcheck /usr/local/bin/

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Build documentation
run: mdbook build
run: ./scripts/mdbook.sh build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
Expand Down
2 changes: 1 addition & 1 deletion book.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ site-url = "https://coder.github.io/httpjail/"
cname = ""
mathjax-support = false
copy-fonts = true
additional-css = []
additional-css = ["docs/custom.css"]
additional-js = ["mermaid.min.js", "mermaid-init.js"]
no-section-label = false
fold.enable = true
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@

# Advanced

- [Request Body Limiting](./advanced/request-body-limiting.md)
- [TLS Interception](./advanced/tls-interception.md)
- [DNS Exfiltration](./advanced/dns-exfiltration.md)
- [Server Mode](./advanced/server-mode.md)
Expand Down
148 changes: 148 additions & 0 deletions docs/advanced/request-body-limiting.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
# Request Body Limiting

The `max_tx_bytes` feature allows you to limit the total size of HTTP requests sent to upstream servers.

This is primarily designed for mitigating code exfiltration attacks through covert channels.

## Size Calculation

The `max_tx_bytes` limit applies to **complete** HTTP requests, including:

1. **Request line**: `METHOD /path HTTP/1.1\r\n`
2. **Headers**: Each header as `Name: Value\r\n`
3. **Header separator**: Final `\r\n` between headers and body
4. **Body**: Request body bytes

## Response Format

To enable request body limiting, return an object with `max_tx_bytes` in your rule response:

```javascript
// JavaScript engine
{allow: {max_tx_bytes: 1024}} // Limit to 1KB total request size
```

```json
// Line processor engine
{"allow": {"max_tx_bytes": 1024}}
```

> **Note**: The `max_tx_bytes` feature is only available in the JavaScript (`--js`) and Line Processor (`--proc`) engines, not in Shell scripts.

## Behavior

The limiting behavior depends on whether the request includes a `Content-Length` header:

### With Content-Length Header

When the request includes a `Content-Length` header (most standard HTTP clients):

1. **Early Detection**: httpjail calculates the total request size
2. **Immediate Rejection**: If it exceeds `max_tx_bytes`, the client receives a `413 Payload Too Large` error immediately
3. **No Upstream Contact**: The upstream server is never contacted, preventing unnecessary load
4. **Clear Feedback**: The error message indicates the actual size and limit

**Example error response:**
```
HTTP/1.1 413 Payload Too Large
Content-Type: text/plain

Request body size (5000 bytes) exceeds maximum allowed (1024 bytes)
```

### Without Content-Length Header

When the request uses chunked encoding or doesn't include `Content-Length`:

1. **Stream Truncation**: The request body is truncated at the limit during streaming
2. **Upstream Receives Partial**: The upstream server receives exactly `max_tx_bytes` total bytes (url + headers + truncated body)
3. **Connection Closes**: The connection terminates after reaching the limit

## Examples

### JavaScript Engine - Upload Endpoint Limiting

```javascript
// Limit upload endpoints to 1KB total request size
const uploadHosts = ['uploads.example.com', 'upload.github.com'];

uploadHosts.includes(r.host)
? {allow: {max_tx_bytes: 1024}}
: r.host.endsWith('.example.com')
```

### Line Processor Engine - Python Example

```python
#!/usr/bin/env python3
import sys, json

upload_hosts = {'uploads.example.com', 'data.api.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
# Returns 413 error if Content-Length exceeds limit
# Truncates body if no Content-Length header
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
elif req['host'].endswith('.example.com'):
print("true")
else:
print("false")
except:
print("false")
sys.stdout.flush()
```


## Use Cases

### 1. Limiting File Uploads

Prevent users from uploading large files to specific endpoints:

```javascript
// JavaScript engine
const uploadPaths = ['/upload', '/api/files'];
uploadPaths.some(path => r.path.startsWith(path))
? {allow: {max_tx_bytes: 10485760}} // 10MB limit
: true
```

### 2. API Cost Control

Limit request sizes to metered APIs to prevent unexpected costs:

```javascript
// JavaScript engine
r.host === 'api.expensive-service.com'
? {allow: {max_tx_bytes: 1024}} // 1KB limit for expensive API
: true
```

### 3. Data Exfiltration Prevention

Prevent large data uploads that might indicate data exfiltration:

```javascript
// JavaScript engine
const externalHosts = ['pastebin.com', 'transfer.sh', 'file.io'];
externalHosts.some(host => r.host.includes(host))
? {allow: {max_tx_bytes: 4096}} // 4KB limit for paste sites
: true
```

## Limitations

- **Shell scripts**: The `max_tx_bytes` feature is not available when using shell script rules (`--shell`)
- **HTTP wire format**: The byte count is based on HTTP wire format, not just the body size
- **Partial uploads**: When truncating (no Content-Length), the upstream server receives incomplete data which may cause application errors

## See Also

- [JavaScript Engine](../guide/rule-engines/javascript.md)
- [Line Processor Engine](../guide/rule-engines/line-processor.md)
- [Configuration](../guide/configuration.md)
22 changes: 22 additions & 0 deletions docs/custom.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/* Minimal CSS to enable multi-line code in tables */
/* Let mdbook theme handle all other styling */

table td pre {
margin: 0;
}

table td pre code {
display: block;
}

/* Make <br> tags work as line breaks in code */
table td pre code br {
display: block;
content: "";
}

/* Keep tables left-aligned - override general.css margin: 0 auto */
.table-wrapper table {
margin-left: 0;
margin-right: 0;
}
11 changes: 6 additions & 5 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,11 @@ allowedHosts.includes(r.host);
httpjail --js-file rules.js -- command
```

## Response Types
## Response Format

Your JavaScript can return:
{{#include ../../includes/response-format-table.md}}

- **Boolean**: `true` to allow, `false` to deny
- **Object with message**: `{allow: false, deny_message: "Custom error"}`
- **Just a message**: `{deny_message: "Blocked"}` (implies deny)
**Examples:**

```javascript
// Simple boolean
Expand All@@ -59,6 +57,9 @@ false // Deny

// Conditional with message
r.host === 'facebook.com' ? {deny_message: 'Social media blocked'} : true

// Limit request upload size to 1KB (headers + body)
({allow: {max_tx_bytes: 1024}})
```

## Common Patterns
Expand Down
14 changes: 10 additions & 4 deletions docs/guide/rule-engines/line-processor.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,11 +27,12 @@ Each request is sent as a single JSON line:

### Response Format

Your processor must respond with one line per request:
Your processor must respond with one line per request.

- **Boolean strings**: `"true"` (allow) or `"false"` (deny)
- **JSON object**: `{"allow": false, "deny_message": "Blocked by policy"}`
- **JSON with message only**: `{"deny_message": "Blocked"}` (implies deny)
{{#include ../../includes/response-format-table.md}}

**Additional:**
- **Boolean strings**: `"true"` (allow) or `"false"` (deny) - same as boolean
- **Any other text**: Treated as deny with that text as the message (e.g., `"Access denied"` becomes a deny with message "Access denied")

## Command Line Usage
Expand All@@ -56,12 +57,17 @@ httpjail --proc "./filter.sh --strict" -- your-command
import sys, json

allowed_hosts = {'github.com', 'api.github.com'}
upload_hosts = {'uploads.example.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in allowed_hosts:
print("true")
elif req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
else:
# Can return JSON for custom messages
response = {"allow": False, "deny_message": f"{req['host']} not allowed"}
Expand Down
9 changes: 9 additions & 0 deletions docs/includes/response-format-table.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
| Response Format | Meaning |
|----------------|---------|
| `true` | Allow the request |
| `false` | Deny the request |
| `{allow: true}` | Allow (object form) |
| `{allow: false}` | Deny (object form) |
| <pre><code>{<br> allow: false,<br> deny_message: "Access denied"<br>}</code></pre> | Deny with custom message |
| `{deny_message: "Blocked"}` | Deny (message implies deny) |
| <pre><code>{<br> allow: {<br> max_tx_bytes: 1024<br> }<br>}</code></pre> | Allow with [request body limiting](../../advanced/request-body-limiting.md) |
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: add support for `max_tx_bytes` as limited allow policy by ammario · Pull Request #75 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 4 additions & 20 deletions .github/workflows/docs.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,27 +23,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: 'latest'

- name: Install mdbook-mermaid
run: |
VERSION=$(curl -s https://api.github.com/repos/badboy/mdbook-mermaid/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -sSL "https://github.com/badboy/mdbook-mermaid/releases/download/${VERSION}/mdbook-mermaid-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" | tar -xz
sudo mv mdbook-mermaid /usr/local/bin/
mdbook-mermaid install .

- name: Install mdbook-linkcheck
run: |
curl -sSL https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/latest/download/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -o linkcheck.zip
unzip -q -o linkcheck.zip
chmod +x mdbook-linkcheck
sudo mv mdbook-linkcheck /usr/local/bin/

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Build documentation
run: mdbook build
run: ./scripts/mdbook.sh build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
Expand Down
2 changes: 1 addition & 1 deletion book.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ site-url = "https://coder.github.io/httpjail/"
cname = ""
mathjax-support = false
copy-fonts = true
additional-css = []
additional-css = ["docs/custom.css"]
additional-js = ["mermaid.min.js", "mermaid-init.js"]
no-section-label = false
fold.enable = true
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@

# Advanced

- [Request Body Limiting](./advanced/request-body-limiting.md)
- [TLS Interception](./advanced/tls-interception.md)
- [DNS Exfiltration](./advanced/dns-exfiltration.md)
- [Server Mode](./advanced/server-mode.md)
Expand Down
148 changes: 148 additions & 0 deletions docs/advanced/request-body-limiting.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
# Request Body Limiting

The `max_tx_bytes` feature allows you to limit the total size of HTTP requests sent to upstream servers.

This is primarily designed for mitigating code exfiltration attacks through covert channels.

## Size Calculation

The `max_tx_bytes` limit applies to **complete** HTTP requests, including:

1. **Request line**: `METHOD /path HTTP/1.1\r\n`
2. **Headers**: Each header as `Name: Value\r\n`
3. **Header separator**: Final `\r\n` between headers and body
4. **Body**: Request body bytes

## Response Format

To enable request body limiting, return an object with `max_tx_bytes` in your rule response:

```javascript
// JavaScript engine
{allow: {max_tx_bytes: 1024}} // Limit to 1KB total request size
```

```json
// Line processor engine
{"allow": {"max_tx_bytes": 1024}}
```

> **Note**: The `max_tx_bytes` feature is only available in the JavaScript (`--js`) and Line Processor (`--proc`) engines, not in Shell scripts.

## Behavior

The limiting behavior depends on whether the request includes a `Content-Length` header:

### With Content-Length Header

When the request includes a `Content-Length` header (most standard HTTP clients):

1. **Early Detection**: httpjail calculates the total request size
2. **Immediate Rejection**: If it exceeds `max_tx_bytes`, the client receives a `413 Payload Too Large` error immediately
3. **No Upstream Contact**: The upstream server is never contacted, preventing unnecessary load
4. **Clear Feedback**: The error message indicates the actual size and limit

**Example error response:**
```
HTTP/1.1 413 Payload Too Large
Content-Type: text/plain

Request body size (5000 bytes) exceeds maximum allowed (1024 bytes)
```

### Without Content-Length Header

When the request uses chunked encoding or doesn't include `Content-Length`:

1. **Stream Truncation**: The request body is truncated at the limit during streaming
2. **Upstream Receives Partial**: The upstream server receives exactly `max_tx_bytes` total bytes (url + headers + truncated body)
3. **Connection Closes**: The connection terminates after reaching the limit

## Examples

### JavaScript Engine - Upload Endpoint Limiting

```javascript
// Limit upload endpoints to 1KB total request size
const uploadHosts = ['uploads.example.com', 'upload.github.com'];

uploadHosts.includes(r.host)
? {allow: {max_tx_bytes: 1024}}
: r.host.endsWith('.example.com')
```

### Line Processor Engine - Python Example

```python
#!/usr/bin/env python3
import sys, json

upload_hosts = {'uploads.example.com', 'data.api.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
# Returns 413 error if Content-Length exceeds limit
# Truncates body if no Content-Length header
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
elif req['host'].endswith('.example.com'):
print("true")
else:
print("false")
except:
print("false")
sys.stdout.flush()
```


## Use Cases

### 1. Limiting File Uploads

Prevent users from uploading large files to specific endpoints:

```javascript
// JavaScript engine
const uploadPaths = ['/upload', '/api/files'];
uploadPaths.some(path => r.path.startsWith(path))
? {allow: {max_tx_bytes: 10485760}} // 10MB limit
: true
```

### 2. API Cost Control

Limit request sizes to metered APIs to prevent unexpected costs:

```javascript
// JavaScript engine
r.host === 'api.expensive-service.com'
? {allow: {max_tx_bytes: 1024}} // 1KB limit for expensive API
: true
```

### 3. Data Exfiltration Prevention

Prevent large data uploads that might indicate data exfiltration:

```javascript
// JavaScript engine
const externalHosts = ['pastebin.com', 'transfer.sh', 'file.io'];
externalHosts.some(host => r.host.includes(host))
? {allow: {max_tx_bytes: 4096}} // 4KB limit for paste sites
: true
```

## Limitations

- **Shell scripts**: The `max_tx_bytes` feature is not available when using shell script rules (`--shell`)
- **HTTP wire format**: The byte count is based on HTTP wire format, not just the body size
- **Partial uploads**: When truncating (no Content-Length), the upstream server receives incomplete data which may cause application errors

## See Also

- [JavaScript Engine](../guide/rule-engines/javascript.md)
- [Line Processor Engine](../guide/rule-engines/line-processor.md)
- [Configuration](../guide/configuration.md)
22 changes: 22 additions & 0 deletions docs/custom.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/* Minimal CSS to enable multi-line code in tables */
/* Let mdbook theme handle all other styling */

table td pre {
margin: 0;
}

table td pre code {
display: block;
}

/* Make <br> tags work as line breaks in code */
table td pre code br {
display: block;
content: "";
}

/* Keep tables left-aligned - override general.css margin: 0 auto */
.table-wrapper table {
margin-left: 0;
margin-right: 0;
}
11 changes: 6 additions & 5 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,11 @@ allowedHosts.includes(r.host);
httpjail --js-file rules.js -- command
```

## Response Types
## Response Format

Your JavaScript can return:
{{#include ../../includes/response-format-table.md}}

- **Boolean**: `true` to allow, `false` to deny
- **Object with message**: `{allow: false, deny_message: "Custom error"}`
- **Just a message**: `{deny_message: "Blocked"}` (implies deny)
**Examples:**

```javascript
// Simple boolean
Expand All@@ -59,6 +57,9 @@ false // Deny

// Conditional with message
r.host === 'facebook.com' ? {deny_message: 'Social media blocked'} : true

// Limit request upload size to 1KB (headers + body)
({allow: {max_tx_bytes: 1024}})
```

## Common Patterns
Expand Down
14 changes: 10 additions & 4 deletions docs/guide/rule-engines/line-processor.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,11 +27,12 @@ Each request is sent as a single JSON line:

### Response Format

Your processor must respond with one line per request:
Your processor must respond with one line per request.

- **Boolean strings**: `"true"` (allow) or `"false"` (deny)
- **JSON object**: `{"allow": false, "deny_message": "Blocked by policy"}`
- **JSON with message only**: `{"deny_message": "Blocked"}` (implies deny)
{{#include ../../includes/response-format-table.md}}

**Additional:**
- **Boolean strings**: `"true"` (allow) or `"false"` (deny) - same as boolean
- **Any other text**: Treated as deny with that text as the message (e.g., `"Access denied"` becomes a deny with message "Access denied")

## Command Line Usage
Expand All@@ -56,12 +57,17 @@ httpjail --proc "./filter.sh --strict" -- your-command
import sys, json

allowed_hosts = {'github.com', 'api.github.com'}
upload_hosts = {'uploads.example.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in allowed_hosts:
print("true")
elif req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
else:
# Can return JSON for custom messages
response = {"allow": False, "deny_message": f"{req['host']} not allowed"}
Expand Down
9 changes: 9 additions & 0 deletions docs/includes/response-format-table.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
| Response Format | Meaning |
|----------------|---------|
| `true` | Allow the request |
| `false` | Deny the request |
| `{allow: true}` | Allow (object form) |
| `{allow: false}` | Deny (object form) |
| <pre><code>{<br> allow: false,<br> deny_message: "Access denied"<br>}</code></pre> | Deny with custom message |
| `{deny_message: "Blocked"}` | Deny (message implies deny) |
| <pre><code>{<br> allow: {<br> max_tx_bytes: 1024<br> }<br>}</code></pre> | Allow with [request body limiting](../../advanced/request-body-limiting.md) |
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat: add support for `max_tx_bytes` as limited allow policy by ammario · Pull Request #75 · coder/httpjail · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 4 additions & 20 deletions .github/workflows/docs.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,27 +23,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: 'latest'

- name: Install mdbook-mermaid
run: |
VERSION=$(curl -s https://api.github.com/repos/badboy/mdbook-mermaid/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -sSL "https://github.com/badboy/mdbook-mermaid/releases/download/${VERSION}/mdbook-mermaid-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" | tar -xz
sudo mv mdbook-mermaid /usr/local/bin/
mdbook-mermaid install .

- name: Install mdbook-linkcheck
run: |
curl -sSL https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/latest/download/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -o linkcheck.zip
unzip -q -o linkcheck.zip
chmod +x mdbook-linkcheck
sudo mv mdbook-linkcheck /usr/local/bin/

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Build documentation
run: mdbook build
run: ./scripts/mdbook.sh build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
Expand Down
2 changes: 1 addition & 1 deletion book.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ site-url = "https://coder.github.io/httpjail/"
cname = ""
mathjax-support = false
copy-fonts = true
additional-css = []
additional-css = ["docs/custom.css"]
additional-js = ["mermaid.min.js", "mermaid-init.js"]
no-section-label = false
fold.enable = true
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@

# Advanced

- [Request Body Limiting](./advanced/request-body-limiting.md)
- [TLS Interception](./advanced/tls-interception.md)
- [DNS Exfiltration](./advanced/dns-exfiltration.md)
- [Server Mode](./advanced/server-mode.md)
Expand Down
148 changes: 148 additions & 0 deletions docs/advanced/request-body-limiting.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
# Request Body Limiting

The `max_tx_bytes` feature allows you to limit the total size of HTTP requests sent to upstream servers.

This is primarily designed for mitigating code exfiltration attacks through covert channels.

## Size Calculation

The `max_tx_bytes` limit applies to **complete** HTTP requests, including:

1. **Request line**: `METHOD /path HTTP/1.1\r\n`
2. **Headers**: Each header as `Name: Value\r\n`
3. **Header separator**: Final `\r\n` between headers and body
4. **Body**: Request body bytes

## Response Format

To enable request body limiting, return an object with `max_tx_bytes` in your rule response:

```javascript
// JavaScript engine
{allow: {max_tx_bytes: 1024}} // Limit to 1KB total request size
```

```json
// Line processor engine
{"allow": {"max_tx_bytes": 1024}}
```

> **Note**: The `max_tx_bytes` feature is only available in the JavaScript (`--js`) and Line Processor (`--proc`) engines, not in Shell scripts.

## Behavior

The limiting behavior depends on whether the request includes a `Content-Length` header:

### With Content-Length Header

When the request includes a `Content-Length` header (most standard HTTP clients):

1. **Early Detection**: httpjail calculates the total request size
2. **Immediate Rejection**: If it exceeds `max_tx_bytes`, the client receives a `413 Payload Too Large` error immediately
3. **No Upstream Contact**: The upstream server is never contacted, preventing unnecessary load
4. **Clear Feedback**: The error message indicates the actual size and limit

**Example error response:**
```
HTTP/1.1 413 Payload Too Large
Content-Type: text/plain

Request body size (5000 bytes) exceeds maximum allowed (1024 bytes)
```

### Without Content-Length Header

When the request uses chunked encoding or doesn't include `Content-Length`:

1. **Stream Truncation**: The request body is truncated at the limit during streaming
2. **Upstream Receives Partial**: The upstream server receives exactly `max_tx_bytes` total bytes (url + headers + truncated body)
3. **Connection Closes**: The connection terminates after reaching the limit

## Examples

### JavaScript Engine - Upload Endpoint Limiting

```javascript
// Limit upload endpoints to 1KB total request size
const uploadHosts = ['uploads.example.com', 'upload.github.com'];

uploadHosts.includes(r.host)
? {allow: {max_tx_bytes: 1024}}
: r.host.endsWith('.example.com')
```

### Line Processor Engine - Python Example

```python
#!/usr/bin/env python3
import sys, json

upload_hosts = {'uploads.example.com', 'data.api.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
# Returns 413 error if Content-Length exceeds limit
# Truncates body if no Content-Length header
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
elif req['host'].endswith('.example.com'):
print("true")
else:
print("false")
except:
print("false")
sys.stdout.flush()
```


## Use Cases

### 1. Limiting File Uploads

Prevent users from uploading large files to specific endpoints:

```javascript
// JavaScript engine
const uploadPaths = ['/upload', '/api/files'];
uploadPaths.some(path => r.path.startsWith(path))
? {allow: {max_tx_bytes: 10485760}} // 10MB limit
: true
```

### 2. API Cost Control

Limit request sizes to metered APIs to prevent unexpected costs:

```javascript
// JavaScript engine
r.host === 'api.expensive-service.com'
? {allow: {max_tx_bytes: 1024}} // 1KB limit for expensive API
: true
```

### 3. Data Exfiltration Prevention

Prevent large data uploads that might indicate data exfiltration:

```javascript
// JavaScript engine
const externalHosts = ['pastebin.com', 'transfer.sh', 'file.io'];
externalHosts.some(host => r.host.includes(host))
? {allow: {max_tx_bytes: 4096}} // 4KB limit for paste sites
: true
```

## Limitations

- **Shell scripts**: The `max_tx_bytes` feature is not available when using shell script rules (`--shell`)
- **HTTP wire format**: The byte count is based on HTTP wire format, not just the body size
- **Partial uploads**: When truncating (no Content-Length), the upstream server receives incomplete data which may cause application errors

## See Also

- [JavaScript Engine](../guide/rule-engines/javascript.md)
- [Line Processor Engine](../guide/rule-engines/line-processor.md)
- [Configuration](../guide/configuration.md)
22 changes: 22 additions & 0 deletions docs/custom.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/* Minimal CSS to enable multi-line code in tables */
/* Let mdbook theme handle all other styling */

table td pre {
margin: 0;
}

table td pre code {
display: block;
}

/* Make <br> tags work as line breaks in code */
table td pre code br {
display: block;
content: "";
}

/* Keep tables left-aligned - override general.css margin: 0 auto */
.table-wrapper table {
margin-left: 0;
margin-right: 0;
}
11 changes: 6 additions & 5 deletions docs/guide/rule-engines/javascript.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,11 @@ allowedHosts.includes(r.host);
httpjail --js-file rules.js -- command
```

## Response Types
## Response Format

Your JavaScript can return:
{{#include ../../includes/response-format-table.md}}

- **Boolean**: `true` to allow, `false` to deny
- **Object with message**: `{allow: false, deny_message: "Custom error"}`
- **Just a message**: `{deny_message: "Blocked"}` (implies deny)
**Examples:**

```javascript
// Simple boolean
Expand All@@ -59,6 +57,9 @@ false // Deny

// Conditional with message
r.host === 'facebook.com' ? {deny_message: 'Social media blocked'} : true

// Limit request upload size to 1KB (headers + body)
({allow: {max_tx_bytes: 1024}})
```

## Common Patterns
Expand Down
14 changes: 10 additions & 4 deletions docs/guide/rule-engines/line-processor.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,11 +27,12 @@ Each request is sent as a single JSON line:

### Response Format

Your processor must respond with one line per request:
Your processor must respond with one line per request.

- **Boolean strings**: `"true"` (allow) or `"false"` (deny)
- **JSON object**: `{"allow": false, "deny_message": "Blocked by policy"}`
- **JSON with message only**: `{"deny_message": "Blocked"}` (implies deny)
{{#include ../../includes/response-format-table.md}}

**Additional:**
- **Boolean strings**: `"true"` (allow) or `"false"` (deny) - same as boolean
- **Any other text**: Treated as deny with that text as the message (e.g., `"Access denied"` becomes a deny with message "Access denied")

## Command Line Usage
Expand All@@ -56,12 +57,17 @@ httpjail --proc "./filter.sh --strict" -- your-command
import sys, json

allowed_hosts = {'github.com', 'api.github.com'}
upload_hosts = {'uploads.example.com'}

for line in sys.stdin:
try:
req = json.loads(line)
if req['host'] in allowed_hosts:
print("true")
elif req['host'] in upload_hosts:
# Limit upload endpoints to 1KB requests
response = {"allow": {"max_tx_bytes": 1024}}
print(json.dumps(response))
else:
# Can return JSON for custom messages
response = {"allow": False, "deny_message": f"{req['host']} not allowed"}
Expand Down
9 changes: 9 additions & 0 deletions docs/includes/response-format-table.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
| Response Format | Meaning |
|----------------|---------|
| `true` | Allow the request |
| `false` | Deny the request |
| `{allow: true}` | Allow (object form) |
| `{allow: false}` | Deny (object form) |
| <pre><code>{<br> allow: false,<br> deny_message: "Access denied"<br>}</code></pre> | Deny with custom message |
| `{deny_message: "Blocked"}` | Deny (message implies deny) |
| <pre><code>{<br> allow: {<br> max_tx_bytes: 1024<br> }<br>}</code></pre> | Allow with [request body limiting](../../advanced/request-body-limiting.md) |
Loading