Skip to content

Commit cc9f25c

Browse files
authored
feat: add support for max_tx_bytes as limited allow policy (#75)
1 parent cb5c602 commit cc9f25c

File tree

19 files changed

+1206
-93
lines changed

19 files changed

+1206
-93
lines changed

.github/workflows/docs.yml

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,11 @@ jobs:
2323
- name: Checkout
2424
uses: actions/checkout@v4
2525

26-
- name: Setup mdBook
27-
uses: peaceiris/actions-mdbook@v2
28-
with:
29-
mdbook-version: 'latest'
30-
31-
- name: Install mdbook-mermaid
32-
run: |
33-
VERSION=$(curl -s https://api.github.com/repos/badboy/mdbook-mermaid/releases/latest | grep tag_name | cut -d '"' -f 4)
34-
curl -sSL "https://github.com/badboy/mdbook-mermaid/releases/download/${VERSION}/mdbook-mermaid-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" | tar -xz
35-
sudo mv mdbook-mermaid /usr/local/bin/
36-
mdbook-mermaid install .
37-
38-
- name: Install mdbook-linkcheck
39-
run: |
40-
curl -sSL https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/latest/download/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -o linkcheck.zip
41-
unzip -q -o linkcheck.zip
42-
chmod +x mdbook-linkcheck
43-
sudo mv mdbook-linkcheck /usr/local/bin/
44-
26+
- name: Setup Rust toolchain
27+
uses: dtolnay/rust-toolchain@stable
28+
4529
- name: Build documentation
46-
run: mdbook build
30+
run: ./scripts/mdbook.sh build
4731

4832
- name: Upload artifact
4933
uses: actions/upload-pages-artifact@v3

book.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ site-url = "https://coder.github.io/httpjail/"
2626
cname = ""
2727
mathjax-support = false
2828
copy-fonts = true
29-
additional-css = []
29+
additional-css = ["docs/custom.css"]
3030
additional-js = ["mermaid.min.js", "mermaid-init.js"]
3131
no-section-label = false
3232
fold.enable = true

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
# Advanced
1818

19+
- [Request Body Limiting](./advanced/request-body-limiting.md)
1920
- [TLS Interception](./advanced/tls-interception.md)
2021
- [DNS Exfiltration](./advanced/dns-exfiltration.md)
2122
- [Server Mode](./advanced/server-mode.md)
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Request Body Limiting
2+
3+
The `max_tx_bytes` feature allows you to limit the total size of HTTP requests sent to upstream servers.
4+
5+
This is primarily designed for mitigating code exfiltration attacks through covert channels.
6+
7+
## Size Calculation
8+
9+
The `max_tx_bytes` limit applies to **complete** HTTP requests, including:
10+
11+
1. **Request line**: `METHOD /path HTTP/1.1\r\n`
12+
2. **Headers**: Each header as `Name: Value\r\n`
13+
3. **Header separator**: Final `\r\n` between headers and body
14+
4. **Body**: Request body bytes
15+
16+
## Response Format
17+
18+
To enable request body limiting, return an object with `max_tx_bytes` in your rule response:
19+
20+
```javascript
21+
// JavaScript engine
22+
{allow: {max_tx_bytes: 1024}} // Limit to 1KB total request size
23+
```
24+
25+
```json
26+
// Line processor engine
27+
{"allow": {"max_tx_bytes": 1024}}
28+
```
29+
30+
> **Note**: The `max_tx_bytes` feature is only available in the JavaScript (`--js`) and Line Processor (`--proc`) engines, not in Shell scripts.
31+
32+
## Behavior
33+
34+
The limiting behavior depends on whether the request includes a `Content-Length` header:
35+
36+
### With Content-Length Header
37+
38+
When the request includes a `Content-Length` header (most standard HTTP clients):
39+
40+
1. **Early Detection**: httpjail calculates the total request size
41+
2. **Immediate Rejection**: If it exceeds `max_tx_bytes`, the client receives a `413 Payload Too Large` error immediately
42+
3. **No Upstream Contact**: The upstream server is never contacted, preventing unnecessary load
43+
4. **Clear Feedback**: The error message indicates the actual size and limit
44+
45+
**Example error response:**
46+
```
47+
HTTP/1.1 413 Payload Too Large
48+
Content-Type: text/plain
49+
50+
Request body size (5000 bytes) exceeds maximum allowed (1024 bytes)
51+
```
52+
53+
### Without Content-Length Header
54+
55+
When the request uses chunked encoding or doesn't include `Content-Length`:
56+
57+
1. **Stream Truncation**: The request body is truncated at the limit during streaming
58+
2. **Upstream Receives Partial**: The upstream server receives exactly `max_tx_bytes` total bytes (url + headers + truncated body)
59+
3. **Connection Closes**: The connection terminates after reaching the limit
60+
61+
## Examples
62+
63+
### JavaScript Engine - Upload Endpoint Limiting
64+
65+
```javascript
66+
// Limit upload endpoints to 1KB total request size
67+
const uploadHosts = ['uploads.example.com', 'upload.github.com'];
68+
69+
uploadHosts.includes(r.host)
70+
? {allow: {max_tx_bytes: 1024}}
71+
: r.host.endsWith('.example.com')
72+
```
73+
74+
### Line Processor Engine - Python Example
75+
76+
```python
77+
#!/usr/bin/env python3
78+
import sys, json
79+
80+
upload_hosts = {'uploads.example.com', 'data.api.com'}
81+
82+
for line in sys.stdin:
83+
try:
84+
req = json.loads(line)
85+
if req['host'] in upload_hosts:
86+
# Limit upload endpoints to 1KB requests
87+
# Returns 413 error if Content-Length exceeds limit
88+
# Truncates body if no Content-Length header
89+
response = {"allow": {"max_tx_bytes": 1024}}
90+
print(json.dumps(response))
91+
elif req['host'].endswith('.example.com'):
92+
print("true")
93+
else:
94+
print("false")
95+
except:
96+
print("false")
97+
sys.stdout.flush()
98+
```
99+
100+
101+
## Use Cases
102+
103+
### 1. Limiting File Uploads
104+
105+
Prevent users from uploading large files to specific endpoints:
106+
107+
```javascript
108+
// JavaScript engine
109+
const uploadPaths = ['/upload', '/api/files'];
110+
uploadPaths.some(path => r.path.startsWith(path))
111+
? {allow: {max_tx_bytes: 10485760}} // 10MB limit
112+
: true
113+
```
114+
115+
### 2. API Cost Control
116+
117+
Limit request sizes to metered APIs to prevent unexpected costs:
118+
119+
```javascript
120+
// JavaScript engine
121+
r.host === 'api.expensive-service.com'
122+
? {allow: {max_tx_bytes: 1024}} // 1KB limit for expensive API
123+
: true
124+
```
125+
126+
### 3. Data Exfiltration Prevention
127+
128+
Prevent large data uploads that might indicate data exfiltration:
129+
130+
```javascript
131+
// JavaScript engine
132+
const externalHosts = ['pastebin.com', 'transfer.sh', 'file.io'];
133+
externalHosts.some(host => r.host.includes(host))
134+
? {allow: {max_tx_bytes: 4096}} // 4KB limit for paste sites
135+
: true
136+
```
137+
138+
## Limitations
139+
140+
- **Shell scripts**: The `max_tx_bytes` feature is not available when using shell script rules (`--shell`)
141+
- **HTTP wire format**: The byte count is based on HTTP wire format, not just the body size
142+
- **Partial uploads**: When truncating (no Content-Length), the upstream server receives incomplete data which may cause application errors
143+
144+
## See Also
145+
146+
- [JavaScript Engine](../guide/rule-engines/javascript.md)
147+
- [Line Processor Engine](../guide/rule-engines/line-processor.md)
148+
- [Configuration](../guide/configuration.md)

docs/custom.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* Minimal CSS to enable multi-line code in tables */
2+
/* Let mdbook theme handle all other styling */
3+
4+
table td pre {
5+
margin: 0;
6+
}
7+
8+
table td pre code {
9+
display: block;
10+
}
11+
12+
/* Make <br> tags work as line breaks in code */
13+
table td pre code br {
14+
display: block;
15+
content: "";
16+
}
17+
18+
/* Keep tables left-aligned - override general.css margin: 0 auto */
19+
.table-wrapper table {
20+
margin-left: 0;
21+
margin-right: 0;
22+
}

docs/guide/rule-engines/javascript.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,11 @@ allowedHosts.includes(r.host);
4141
httpjail --js-file rules.js -- command
4242
```
4343

44-
## Response Types
44+
## Response Format
4545

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

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

5250
```javascript
5351
// Simple boolean
@@ -59,6 +57,9 @@ false // Deny
5957

6058
// Conditional with message
6159
r.host === 'facebook.com' ? {deny_message: 'Social media blocked'} : true
60+
61+
// Limit request upload size to 1KB (headers + body)
62+
({allow: {max_tx_bytes: 1024}})
6263
```
6364

6465
## Common Patterns

docs/guide/rule-engines/line-processor.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ Each request is sent as a single JSON line:
2727

2828
### Response Format
2929

30-
Your processor must respond with one line per request:
30+
Your processor must respond with one line per request.
3131

32-
- **Boolean strings**: `"true"` (allow) or `"false"` (deny)
33-
- **JSON object**: `{"allow": false, "deny_message": "Blocked by policy"}`
34-
- **JSON with message only**: `{"deny_message": "Blocked"}` (implies deny)
32+
{{#include ../../includes/response-format-table.md}}
33+
34+
**Additional:**
35+
- **Boolean strings**: `"true"` (allow) or `"false"` (deny) - same as boolean
3536
- **Any other text**: Treated as deny with that text as the message (e.g., `"Access denied"` becomes a deny with message "Access denied")
3637

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

5859
allowed_hosts = {'github.com', 'api.github.com'}
60+
upload_hosts = {'uploads.example.com'}
5961

6062
for line in sys.stdin:
6163
try:
6264
req = json.loads(line)
6365
if req['host'] in allowed_hosts:
6466
print("true")
67+
elif req['host'] in upload_hosts:
68+
# Limit upload endpoints to 1KB requests
69+
response = {"allow": {"max_tx_bytes": 1024}}
70+
print(json.dumps(response))
6571
else:
6672
# Can return JSON for custom messages
6773
response = {"allow": False, "deny_message": f"{req['host']} not allowed"}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
| Response Format | Meaning |
2+
|----------------|---------|
3+
| `true` | Allow the request |
4+
| `false` | Deny the request |
5+
| `{allow: true}` | Allow (object form) |
6+
| `{allow: false}` | Deny (object form) |
7+
| <pre><code>{<br> allow: false,<br> deny_message: "Access denied"<br>}</code></pre> | Deny with custom message |
8+
| `{deny_message: "Blocked"}` | Deny (message implies deny) |
9+
| <pre><code>{<br> allow: {<br> max_tx_bytes: 1024<br> }<br>}</code></pre> | Allow with [request body limiting](../../advanced/request-body-limiting.md) |

0 commit comments

Comments
 (0)