ESP32 HTTP API がクエリパラメータでファイルパス(?path=config.json)を受け取る場合、ファイルシステムに触れる前に URL デコードしてトラバーサル試行を拒否しなければならない。そうしないと ?path=../x や ?path=%2e%2e%2f で LittleFS マウントから脱出される。
URL デコーダ
クエリ文字列は特殊バイトを %XX、スペースを + でエンコードする。最小限のデコーダ:
HTTPHandlersFiles.cpp
std::string url_decode(const std::string &in) {
std::string out;
out.reserve(in.size());
for (size_t i = 0; i < in.size(); ++i) {
char c = in[i];
if (c == '%' && i + 2 < in.size()) {
char hex[3] = { in[i+1], in[i+2], '\0' };
char decoded = static_cast<char>(strtol(hex, NULL, 16));
out.push_back(decoded);
i += 2;
} else if (c == '+') {
out.push_back(' ');
} else {
out.push_back(c);
}
}
return out;
}パスサニタイザ
先にデコードし、それからサニタイズ——サニタイズ後にデコードすると %2e%2e%2f(../)を見逃す。クエリ/フラグメントの残りを取り除き、パスをマウントポイントに対して相対に強制し、すべての .. を拒否する:
HTTPHandlersFiles.cpp
bool sanitize_path(const std::string &in, std::string &out) {
std::string dec = url_decode(in);
size_t qpos = dec.find('?');
if (qpos != std::string::npos) dec = dec.substr(0, qpos);
size_t hpos = dec.find('#');
if (hpos != std::string::npos) dec = dec.substr(0, hpos);
size_t start = 0;
while (start < dec.size() && dec[start] == '/') start++; // 相対に強制
out = dec.substr(start);
if (out.find("..") != std::string::npos) return false;
return true;
}ファイルシステムに触れるすべてのハンドラで使う:
usage.cpp
std::string rel = parser.GetParameter("path");
if (!sanitize_path(rel, rel) || rel.empty()) {
httpd_resp_set_status(req, "400 Bad Request");
httpd_resp_sendstr(req, "{\"error\": \"Invalid path\"}");
return ESP_OK;
}
std::string fullpath = std::string(LittleFSManager::mountPoint()) + "/" + rel;注意点
- すべての
..部分文字列を拒否するのは必要以上に厳格(foo..barも拒否される)が、組込み API ではエッジケースの正確さより単純さが勝る。 - システムファイル用に拒否リストを保持し(例:
nodes.json)、有効なパスでもファームウェアが管理する状態を削除できないようにする。 - クエリパース自体には
httpd_req_get_url_query_str+httpd_query_key_value、または小さなQueryURLParserラッパがうまく機能する。
関連記事
この記事が役に立った場合は、TechOverflow での新しい記事の調査と公開を支援するため、「Buy Me a Coffee」でのご支援、または PayPal 経由でのご寄付をご検討ください