Every request from the CLI to the proxy includes a JWT signed with a shared secret using HMAC-SHA256.
HEADER.PAYLOAD.SIGNATURE
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"iat": 1707550000, "exp": 1707550300, "jti": "550e8400-e29b-41d4-a716-446655440000"}
| Property | Value | Purpose |
|---|---|---|
| Algorithm | HS256 (HMAC-SHA256) | Symmetric signature |
| Expiry | 5 minutes (iat + 300) |
Limits replay window |
| Clock Skew Tolerance | 30 seconds | Handles time drift between client and server |
| JTI | UUID v4 | Unique per request; prevents replay within the 5-minute window |
- Split token into 3 parts (header, payload, signature)
- Decode and validate header (
algmust beHS256) - Compute HMAC-SHA256 of
header.payloadusing the shared secret - Constant-time compare computed signature with provided signature
- Decode payload and check
exp(with 30s clock skew tolerance) - Check
iatis not in the future (with 30s tolerance) - Check JTI against CacheService for replay detection (5-minute TTL)
- Minimum length: 32 bytes (256 bits) recommended
- Generation: Cryptographic random (e.g.,
openssl rand -base64 48) - Storage (CLI):
~/.gproxy/config.jsonwith file permissions0600 - Storage (Proxy): Apps Script Script Properties (not in source code)
- Rotation: Change the secret in both Script Properties and CLI config; old tokens expire within 5 minutes
Both the CLI (Node.js) and proxy (Apps Script) implement base64url encoding/decoding per RFC 7515:
- Replace
+with- - Replace
/with_ - Strip trailing
=padding
The CLI uses Buffer.toString('base64') with regex replacement. The proxy uses Utilities.base64Encode() with the same replacement pattern. Both implementations produce identical output for the same input, ensuring interoperability.
Signature verification uses constant-time string comparison to prevent timing attacks:
function constantTimeCompare(a, b) {
if (typeof a !== 'string' || typeof b !== 'string') return false;
if (a.length !== b.length) return false;
var result = 0;
for (var i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}Note: The length check leaks whether strings are the same length. For HMAC signatures of a fixed algorithm, all outputs have the same length, so this is not a concern.
Apps Script doPost(e) does not expose the client's real IP address. The IP used for allowlisting is client-reported via the clientIp field in the request body. A motivated attacker with the shared secret could spoof any IP.
This is defense-in-depth only — it prevents casual misuse but does not constitute a security boundary.
Set IP_ALLOWLIST in Script Properties as a comma-separated list:
192.168.1.100,10.0.0.0/8,203.0.113.42
Supports:
- Exact IPv4 match:
192.168.1.100 - CIDR notation:
10.0.0.0/8
When enabled, the proxy queries AbuseIPDB's threat intelligence API before processing requests:
| Script Property | Description |
|---|---|
IP_CHECK_ENABLED |
true to enable |
IP_CHECK_API_KEY |
AbuseIPDB API key |
IP_CHECK_THRESHOLD |
Confidence score threshold (default: 50) |
The check fails open — if AbuseIPDB is unreachable, the request proceeds. This prioritizes availability over security for this defense-in-depth layer.
The CLI validates that the proxy URL starts with https://. HTTP URLs are rejected with an error. This ensures all traffic between the CLI and Apps Script is encrypted in transit.
The proxy logger (Logger.gs) never records request parameters. Log entries include only:
| Column | Example |
|---|---|
| Timestamp | 2026-02-10T14:30:00.000Z |
| Request ID | a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
| Source IP | 192.168.1.100 |
| Service | gmail |
| Action | send |
| Status | OK |
| Duration (ms) | 1234 |
| Error | (empty on success) |
Email bodies, document content, file data, and other sensitive parameters are never written to the log sheet.
The proxy runs as a Web App deployed as the deploying user with access set to Anyone. This means:
- All Google API calls execute with the permissions of the user who deployed the script
- The "Anyone" access setting means the web app URL is publicly reachable (JWT auth is the access control)
- OAuth scopes are granted at deployment time and apply to all requests
| Secret | Where Stored | Notes |
|---|---|---|
| JWT shared secret | CLI: ~/.gproxy/config.json (mode 0600) |
Never committed to source control |
| JWT shared secret | Proxy: Script Properties (JWT_SECRET) |
Not accessible via web; only via Apps Script editor |
| AbuseIPDB API key | Proxy: Script Properties (IP_CHECK_API_KEY) |
Optional; redacted in getAllConfig() |
| Threat | Mitigation |
|---|---|
| Unauthorized API access | JWT authentication with shared secret |
| Token replay | JTI uniqueness check via CacheService (5-minute window) |
| Token expiry bypass | 5-minute expiry with 30-second clock skew tolerance |
| Timing attacks on signature | Constant-time comparison |
| Eavesdropping | HTTPS-only communication |
| Log data leakage | Content redaction (no request params logged) |
| Casual IP-based attacks | IP allowlisting (defense-in-depth) |
| Brute force secret guessing | 32+ byte secret provides 256+ bits of entropy |
| Threat | Why |
|---|---|
| Compromised shared secret | Anyone with the secret can authenticate. Rotate immediately if compromised. |
| Compromised Google account | The proxy runs as the deploying user. If that account is compromised, all API access is exposed. |
| IP spoofing | Client-reported IP is not verified by Apps Script |
| Apps Script platform compromise | GProxy relies on Google's infrastructure security |
| Insider threats | Anyone with access to the Apps Script project can read the secret from Script Properties |
| Rate limiting | GProxy does not implement its own rate limiting beyond Google's API quotas |
- Use a strong, randomly generated shared secret (32+ bytes)
- Restrict Script Properties access to trusted editors only
- Enable IP allowlisting as an additional layer
- Monitor the log sheet for unusual activity
- Rotate the secret periodically and immediately if compromise is suspected
- Use a dedicated Google account for the proxy if managing sensitive data