ライフサイクル攻撃対応プロキシ
npmやmavenのライブラリが汚染されているかもしれないっていう不安から、自前のプロキシサーバを構築して対応しようと考え、以下の構成で実装してみました。
構成
[ 開発PC / CI/CD ]
│ (mvn install / npm install)
▼
[ 検疫プロキシ (Python) ] ※ここで「14日以内」をチェックして弾く
│ (安全なら転送)
▼
[ Nexus OSS (Proxy) ]
│ (キャッシュがなければ取得)
▼
[ 外部 (Maven Central / npmレジストリ) ]
dockerの設定
docker-compose.yml
services:
# Nexus本体
nexus:
image: sonatype/nexus3:latest
container_name: nexus
restart: always
volumes:
- ./.docker/nexus-data:/nexus-data
ports:
- "61012:8081" # Nexus管理画面 兼 キャッシュサーバー
networks:
- proxy-net
# 自作の検疫プロキシ(Python)
quarantine-proxy:
image: python:3.11-slim
container_name: quarantine-proxy
restart: always
ports:
- "61013:8080" # クライアント接続ポート
volumes:
- ./proxy_script:/app
working_dir: /app
command: sh -c "pip install flask requests && python app.py"
networks:
- proxy-net
depends_on:
- nexus
networks:
proxy-net:
volumes:
nexus-data:
あらかじめ、以下のディレクトリは200にしておきます。(nexusがうまく動かなかった)
sudo chown -R 200:200 ./.docker/nexus-data
python→nexus接続ユーザの作成
pythonスクリプト
※このスクリプトはあくまでサンプルですので、参考程度という事でお願いします。
正常動作しなかった場合はご了承ください。
./proxy_script/app.py
import base64
from datetime import datetime, timedelta
import re
from flask import Flask, Response, request
import requests
app = Flask(__name__)
# ==========================================
# 【設定情報】
# ==========================================
NEXUS_URL = "http://192.168.x.x:61012"
PROXY_USER = "proxy-user"
PROXY_PASSWORD = "xxxxxxx"
HOLD_DAYS = 14 # 検疫期間(14日間)
# Nexusへの認証ヘッダーを事前に作成 (Basic認証)
token = base64.b64encode(f"{PROXY_USER}:{PROXY_PASSWORD}".encode("utf-8")).decode(
"utf-8"
)
NEXUS_HEADERS = {"Authorization": f"Basic {token}"}
# ==========================================
# 【検疫ロジック】公開日数をチェックする関数
# ==========================================
def is_safe_component(repo_type, path):
try:
# --- ① npm の検疫判定 ---
if repo_type == "npm":
# パスからパッケージ名とバージョンを抽出する正規表現
# 例: /repository/npm-public/vue/-/vue-3.5.0.tgz -> name: vue, version: 3.5.0
match = re.search(r"([^/]+)/-/.*-(\d+\.\d+\.\d+(-\w+)?)\.tgz", path)
if not match:
return True # 該当のメタデータ構造でない場合は一旦通す(またはメタデータjson)
package_name, version = match.group(1), match.group(2)
# スコープ付きパッケージ(例: @types/node)のハンドリング
if "/@" in path:
scope_match = re.search(r"(@[^/]+/[^/]+)/-/.*", path)
if scope_match:
package_name = scope_match.group(1)
# npm公式APIに問い合わせ
url = f"https://registry.npmjs.org/{package_name}"
res = requests.get(url, timeout=5).json()
time_str = res.get("time", {}).get(version)
if time_str:
# ISO 8601形式 (2026-06-19T...Z) をパース
release_date = datetime.strptime(time_str[:10], "%Y-%m-%d")
if datetime.now() - release_date < timedelta(days=HOLD_DAYS):
print(
f"[⚠️ BLOCK] npm: {package_name}@{version} はリリース後 {HOLD_DAYS} 日以内です。"
)
return False
# --- ② Maven の検疫判定 ---
elif repo_type == "maven":
if not path.endswith((".jar", ".pom")):
return True
# 先頭の "/repository/リポジトリ名/" を取り除く
match_maven = re.match(r"^/repository/[^/]+/(.*)$", path)
maven_path = match_maven.group(1) if match_maven else path.lstrip("/")
# Maven Central 本尊のURLを構築
central_url = f"https://repo1.maven.org/maven2/{maven_path}"
# Maven Central本体に直接 HEAD リクエストを送り、ファイルの正確なタイムスタンプを取得する
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
central_res = requests.head(central_url, headers=headers, timeout=5)
if central_res.status_code == 200:
last_modified_str = central_res.headers.get("Last-Modified")
if last_modified_str:
from email.utils import parsedate_to_datetime
release_date = parsedate_to_datetime(last_modified_str).replace(tzinfo=None)
if datetime.now() - release_date < timedelta(days=HOLD_DAYS):
print(f"[⚠️ BLOCK] Maven: {maven_path} はリリース後 {HOLD_DAYS} 日以内です。({release_date})")
return False
else:
print(f"[INFO] Maven: {maven_path} は14日以上経過しているため安全です。({release_date})")
return True
# 安全側に倒して、検疫ブロックします
print(f"[⚠️ BLOCK] Maven: {maven_path} は Maven Central 上に存在しない、または確認できないためブロックします。")
return False
except Exception as e:
# APIエラー等の場合は安全側に倒してブロック
print(f"[ERROR] 検疫チェック中にエラーが発生: {e}")
return False
return True
# ==========================================
# 【プロキシメイン処理】どんなパスでも一律で受け止める
# ==========================================
@app.route("/", defaults={"path": ""}, methods=["GET", "HEAD"])
@app.route("/<path:path>", methods=["GET", "HEAD"])
def catch_all(path):
# 頭にスラッシュをつけて、重複するスラッシュ(//)を1つに綺麗に成形する
full_path = "/" + path
full_path = re.sub(r"//+", "/", full_path)
match = re.match(r"^/repository/([^/]+)/(.*)$", full_path)
if not match:
target_url = f"{NEXUS_URL}{full_path}"
else:
repo_name = match.group(1)
target_path = match.group(2)
repo_type = "unknown"
if "npm" in repo_name:
repo_type = "npm"
elif "maven" in repo_name:
repo_type = "maven"
if request.method in ["GET", "HEAD"]:
if not is_safe_component(repo_type, full_path):
return Response(
"🚫 [Quarantine] Access Denied: Under 14-day quarantine.",
status=403,
)
target_url = f"{NEXUS_URL}{full_path}"
if request.query_string:
target_url += f"?{request.query_string.decode('utf-8')}"
try:
# Nexusへリレー通信
nexus_response = requests.request(
method=request.method,
url=target_url,
auth=(PROXY_USER, PROXY_PASSWORD),
stream=True,
timeout=30,
)
if request.method == "HEAD":
response = Response(
status=nexus_response.status_code,
content_type=nexus_response.headers.get("Content-Type"),
)
for key, value in nexus_response.headers.items():
if key.lower() not in [
"content-length",
"transfer-encoding",
"connection",
]:
response.headers[key] = value
return response
# --- GETリクエストの処理 ---
content_type = nexus_response.headers.get("Content-Type", "")
if "application/json" in content_type and "/repository/" in full_path:
proxy_root_url = request.host_url.rstrip("/")
json_text = nexus_response.text
modified_json_text = json_text.replace(NEXUS_URL, proxy_root_url)
response = Response(
modified_json_text,
status=nexus_response.status_code,
content_type=content_type,
)
return response
response = Response(
nexus_response.iter_content(chunk_size=10 * 1024),
status=nexus_response.status_code,
content_type=content_type,
)
return response
except Exception as e:
import traceback
print(f"[🚨 CRITICAL ERROR]\n{traceback.format_exc()}")
return Response("Internal Proxy Error", status=500)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)
動作確認 maven
クライアント設定
クライアント側では、~/.m2/settings.xmlを作成してプロキシ経由にします
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
<mirrors>
<mirror>
<id>company-quarantine-mirror</id>
<name>Company Quarantine Proxy Mirror</name>
<url>http://192.168.x.x:61013/repository/maven-public/</url>
<mirrorOf>*</mirrorOf>
</mirror>
</mirrors>
<profiles>
<profile>
<id>quarantine-profile</id>
<repositories>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>quarantine-profile</activeProfile>
</activeProfiles>
</settings>
jackson-databindで実験します。
- 3.1.4 は 2026/5/29 なので、今日(2026/6/20)より14以上前なので正常に取れる
% mvn dependency:get -Dartifact=tools.jackson.core:jackson-databind:3.1.4
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------< org.apache.maven:standalone-pom >-------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] --------------------------------[ pom ]---------------------------------
[INFO]
[INFO] --- dependency:3.6.1:get (default-cli) @ standalone-pom ---
[INFO] Resolving tools.jackson.core:jackson-databind:jar:3.1.4 with transitive dependencies
Downloading from company-quarantine-mirror: http://192.168.x.x:61013/repository/maven-public/tools/jackson/core/jackson-databind/3.1.4/jackson-databind-3.1.4.pom
Downloaded from company-quarantine-mirror: http://192.168.x.x:61013/repository/maven-public/tools/jackson/core/jackson-databind/3.1.4/jackson-databind-3.1.4.pom (18 kB at 52 kB/s)
<中略>
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.867 s
[INFO] Finished at: 2026-06-20T20:09:14+09:00
[INFO] ------------------------------------------------------------------------
- 3.2.0 は 2026/6/9 なので、今日(2026/6/20)より14日以内なので取れない
% mvn dependency:get -Dartifact=tools.jackson.core:jackson-databind:3.2.0
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------< org.apache.maven:standalone-pom >-------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] --------------------------------[ pom ]---------------------------------
[INFO]
[INFO] --- dependency:3.6.1:get (default-cli) @ standalone-pom ---
[INFO] Resolving tools.jackson.core:jackson-databind:jar:3.2.0 with transitive dependencies
Downloading from company-quarantine-mirror: http://192.168.x.x:61013/repository/maven-public/tools/jackson/core/jackson-databind/3.2.0/jackson-databind-3.2.0.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.873 s
[INFO] Finished at: 2026-06-20T20:08:26+09:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-dependency-plugin:3.6.1:get (default-cli) on project standalone-pom: Couldn't download artifact: org.eclipse.aether.resolution.DependencyResolutionException: Failed to read artifact descriptor for tools.jackson.core:jackson-databind:jar:3.2.0: The following artifacts could not be resolved: tools.jackson.core:jackson-databind:pom:3.2.0 (absent): Could not transfer artifact tools.jackson.core:jackson-databind:pom:3.2.0 from/to company-quarantine-mirror (http://192.168.x.x:61013/repository/maven-public/): status code: 403, reason phrase: FORBIDDEN (403) -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
動作確認 npm
クライアント設定
クライアント側では、.npmrcを作成してプロキシ経由にします
.npmrc
registry=http://192.168.x.x:61013/repository/npm-public/
//192.168.x.x:61013/repository/npm-public/:_auth=false
@aws-sdk/client-s3で実験します。
- 3.1063.0 は 15 days ago で14以上前なので正常に取れる
>npm install @aws-sdk/client-s3@3.1063.0
added 45 packages in 5s
7 packages are looking for funding
run `npm fund` for details
- 3.1064.0 は 12 days ago で14日以内なので取れない
>npm install @aws-sdk/client-s3@3.1064.0
npm error code E403
npm error 403 403 FORBIDDEN - GET http://192.168.x.x:61013/repository/npm-public/@aws-sdk/client-s3/-/client-s3-3.1064.0.tgz
npm error 403 In most cases, you or one of your dependencies are requesting
npm error 403 a package version that is forbidden by your security policy, or
npm error 403 on a server you do not have access to.
npm error A complete log of this run can be found in: C:\Users\xxxxx\AppData\Local\npm-cache\_logs\2026-06-20T11_44_02_306Z-debug-0.log
あとがき
14日とした理由は特にないが、一旦これで運用してみて課題がありそうだったら考える予定です


