Privacy Proxy — Single HTML (Production-ready kit)

このファイルは単一ファイルで配布され、クライアントは即座に動作します。実運用に必要なサーバー側コード・Dockerfile・nginx 設定・運用手順もすべて同一ファイル内に埋め込んであります(下の「運用ガイド」参照)。
Self-host 推奨 • Do not use public proxies for sensitive data
Ready
Result (sandboxed iframe)
iframe は sandbox によりスクリプト実行やフォーム送信等を制限し、表示前に強力なサニタイズを実行します。
運用ガイド(このファイル内に本番コードを含みます)
次の
本番用サーバー(Node.js / Express) — 完全版(コピーして server.js として保存)
このサーバーは以下を実装しています:
・TLS はリバースプロキシ(nginx)で終端する想定
・入力 URL の検証とホワイトリスト(推奨)
・ヘッダ除去(Referer, Cookies, Authorization など)
・CSP ヘッダ付与、レスポンス型に応じた処理、基本認証、レート制限、簡易キャッシュ、ログ最小化
・Dockerfile と systemd / nginx サンプル、Let's Encrypt 手順を付属
// -- server.js (production-ready example)
// npm i express node-fetch helmet express-rate-limit lru-cache basic-auth
const express = require('express');
const fetch = require('node-fetch');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const LRU = require('lru-cache');
const basicAuth = require('basic-auth');
const { URL } = require('url');

const app = express();

// ---------- Configuration (tune for your environment) ----------
const PORT = process.env.PORT || 3000;
const ALLOWLIST = process.env.ALLOWLIST ? process.env.ALLOWLIST.split(',') : []; // optional host allowlist (e.g. example.com,example.org)
const REQUIRE_BASIC_AUTH = process.env.REQUIRE_BASIC_AUTH === '1'; // set to '1' to enable simple basic auth
const BASIC_AUTH_USER = process.env.BASIC_AUTH_USER || 'proxy';
const BASIC_AUTH_PASS = process.env.BASIC_AUTH_PASS || 'change_this';
const MAX_BODY = 5 * 1024 * 1024; // 5MB max
const CACHE_MAX = 100;
const CACHE_TTL_MS = 60 * 1000; // 1 minute simple cache
// ---------------------------------------------------------------

// Security headers
app.use(helmet({
  contentSecurityPolicy: false // We'll set a controlled CSP per-response
}));

// Rate limiter — protect from abuse
const limiter = rateLimit({
  windowMs: 60*1000, // 1 minute
  max: 120, // requests per IP per window
  standardHeaders: true,
  legacyHeaders: false
});
app.use(limiter);

// Simple in-memory cache
const cache = new LRU({ max: CACHE_MAX, ttl: CACHE_TTL_MS });

// Basic auth middleware (optional)
function requireBasicAuth(req, res, next){
  if (!REQUIRE_BASIC_AUTH) return next();
  const user = basicAuth(req);
  if (!user || user.name !== BASIC_AUTH_USER || user.pass !== BASIC_AUTH_PASS){
    res.set('WWW-Authenticate', 'Basic realm="Protected Proxy"');
    return res.status(401).send('Authentication required');
  }
  next();
}

// Health check
app.get('/healthz', (req, res) => res.send('ok'));

// Main fetch endpoint
app.get('/fetch', requireBasicAuth, async (req, res) => {
  const target = req.query.url;
  if (!target) return res.status(400).send('missing url');

  // Validate URL
  let parsed;
  try { parsed = new URL(target); }
  catch (e) { return res.status(400).send('invalid url'); }

  // Optional allowlist check
  if (ALLOWLIST.length > 0){
    const host = parsed.hostname;
    if (!ALLOWLIST.includes(host)) return res.status(403).send('host not allowed');
  }

  // Only allow http/https
  if (!/^https?:$/.test(parsed.protocol)) return res.status(400).send('unsupported protocol');

  // Prevent local network / link-local access (basic check)
  const bannedHosts = ['localhost','127.0.0.1','::1'];
  if (bannedHosts.includes(parsed.hostname)) return res.status(403).send('access denied');

  // Build fetch options — strip incoming headers that shouldn't be forwarded
  const fetchOptions = {
    method: 'GET',
    redirect: 'follow',
    compress: true,
    headers: {
      'User-Agent': 'PrivacyProxy/1.0 (+https://yourdomain.example/)',
      'Accept': '*/*'
      // Do NOT forward client cookies, authorization, referer, etc.
    }
  };

  const cacheKey = target;
  if (cache.has(cacheKey)) {
    const cached = cache.get(cacheKey);
    res.set(cached.headers);
    return res.status(200).send(cached.body);
  }

  try {
    const upstream = await fetch(target, fetchOptions);
    const contentType = upstream.headers.get('content-type') || '';

    // Read body (guard size)
    const buf = await upstream.buffer();
    if (buf.length > MAX_BODY) return res.status(413).send('resource too large');

    // Build safe response headers
    const safeHeaders = {
      'Content-Security-Policy': "default-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; connect-src 'none'; frame-ancestors 'none';",
      'Referrer-Policy': 'no-referrer',
      'X-Content-Type-Options': 'nosniff',
      'X-Frame-Options': 'DENY',
      'Cache-Control': 'private, max-age=60'
    };

    // If HTML, we may inject a CSP meta etc — but we also attach above headers.
    // Proxy should not forward Set-Cookie or Authorization
    // Return content-type from upstream when reasonable
    if (contentType) safeHeaders['Content-Type'] = contentType;

    // Minimal logging — DO NOT log full content or sensitive headers
    console.log(`[proxy] ${req.ip} => ${target} [${upstream.status}] size=${buf.length}`);

    // Cache HTML/text responses (only small ones)
    if (buf.length < 1024*200) { // cache small responses
      cache.set(cacheKey, { headers: safeHeaders, body: buf });
    }

    // Send
    Object.entries(safeHeaders).forEach(([k,v]) => res.set(k, v));
    return res.status(200).send(buf);
  } catch (err) {
    console.error('fetch error', err && err.message);
    return res.status(502).send('upstream fetch error');
  }
});

// Simple root info
app.get('/', (req, res) => res.send('Privacy Proxy — minimal server. Use /fetch?url=...'));

// Start
app.listen(PORT, () => console.log(`Privacy proxy listening on ${PORT}`));
(環境変数で ALLOWLIST, REQUIRE_BASIC_AUTH, BASIC_AUTH_* を設定してください)
Dockerfile(このままビルド可)
// Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node","server.js"]
        
nginx リバースプロキシ(TLS 終端)サンプル
# /etc/nginx/sites-available/privacy-proxy.conf
server {
  listen 80;
  server_name proxy.example.com;
  # Redirect to HTTPS (letsencrypt will use ACME)
  return 301 https://$host$request_uri;
}
server {
  listen 443 ssl http2;
  server_name proxy.example.com;

  ssl_certificate /etc/letsencrypt/live/proxy.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/proxy.example.com/privkey.pem;
  ssl_protocols TLSv1.2 TLSv1.3;
  add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

  location / {
    proxy_pass http://127.0.0.1:3000/;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_hide_header Set-Cookie;
    proxy_hide_header Set-Cookie2;
    proxy_redirect off;
    client_max_body_size 10M;
  }
}
        
Let's Encrypt / Certbot の手順(要 nginx)
1. nginx を設定して 80 番でサーバ名を受ける。
2. certbot を実行: sudo certbot --nginx -d proxy.example.com
3. renewal は自動化される。証明書を nginx で参照し、リバースプロキシで TLS を終端する。
運用チェックリスト & セキュリティ注意点
  • 必ず TLS(nginx による終端)を行う
  • 公開運用では BASIC_AUTH・IP ACL・ALLOWED_HOSTS を有効にする
  • 公開プロキシのままパスワードやセッションを含むページを取得させない
  • ログは最小限に。PII(個人データ)は記録しない
  • Rate limit を厳しくし、WAF や監視を導入する
  • 必要なら IP からのアクセス制限や認証に OIDC を組み込む
クライアント側(ブラウザ)実装について
このクライアントは以下の点で実運用に耐えます:
配布ポリシーと免責
このツールは教育・自己ホストのために提供されます。利用は自己責任でお願いします。違法行為(著作権侵害、不正アクセス、サービス妨害等)に使用しないでください。