Software /
code /
prosody
File
tools/ejabberdsql2prosody.lua @ 12181:783056b4e448 0.11 0.11.12
util.xml: Do not allow doctypes, comments or processing instructions
Yes. This is as bad as it sounds. CVE pending.
In Prosody itself, this only affects mod_websocket, which uses util.xml
to parse the <open/> frame, thus allowing unauthenticated remote DoS
using Billion Laughs. However, third-party modules using util.xml may
also be affected by this.
This commit installs handlers which disallow the use of doctype
declarations and processing instructions without any escape hatch. It,
by default, also introduces such a handler for comments, however, there
is a way to enable comments nontheless.
This is because util.xml is used to parse human-facing data, where
comments are generally a desirable feature, and also because comments
are generally harmless.
author | Jonas Schäfer <jonas@wielicki.name> |
---|---|
date | Mon, 10 Jan 2022 18:23:54 +0100 |
parent | 7568:4f7990902874 |
child | 13142:879a6a33c21b |
line wrap: on
line source
#!/usr/bin/env lua -- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- prosody = {}; package.path = package.path ..";../?.lua"; local my_name = arg[0]; if my_name:match("[/\\]") then package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua"); package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so"); end local serialize = require "util.serialization".serialize; local st = require "util.stanza"; local parse_xml = require "util.xml".parse; package.loaded["util.logger"] = {init = function() return function() end; end} local dm = require "util.datamanager" dm.set_data_path("data"); function parseFile(filename) ------ local file = nil; local last = nil; local line = 1; local function read(expected) local ch; if last then ch = last; last = nil; else ch = file:read(1); if ch == "\n" then line = line + 1; end end if expected and ch ~= expected then error("expected: "..expected.."; got: "..(ch or "nil").." on line "..line); end return ch; end local function peek() if not last then last = read(); end return last; end local escapes = { ["\\0"] = "\0"; ["\\'"] = "'"; ["\\\""] = "\""; ["\\b"] = "\b"; ["\\n"] = "\n"; ["\\r"] = "\r"; ["\\t"] = "\t"; ["\\Z"] = "\26"; ["\\\\"] = "\\"; ["\\%"] = "%"; ["\\_"] = "_"; } local function unescape(s) return escapes[s] or error("Unknown escape sequence: "..s); end local function readString() read("'"); local s = ""; while true do local ch = peek(); if ch == "\\" then s = s..unescape(read()..read()); elseif ch == "'" then break; else s = s..read(); end end read("'"); return s; end local function readNonString() local s = ""; while true do if peek() == "," or peek() == ")" then break; else s = s..read(); end end return tonumber(s); end local function readItem() if peek() == "'" then return readString(); else return readNonString(); end end local function readTuple() local items = {} read("("); while peek() ~= ")" do table.insert(items, readItem()); if peek() == ")" then break; end read(","); end read(")"); return items; end local function readTuples() if peek() ~= "(" then read("("); end local tuples = {}; while true do table.insert(tuples, readTuple()); if peek() == "," then read() end if peek() == ";" then break; end end return tuples; end local function readTableName() local tname = ""; while peek() ~= "`" do tname = tname..read(); end return tname; end local function readInsert() if peek() == nil then return nil; end for ch in ("INSERT INTO `"):gmatch(".") do -- find line starting with this if peek() == ch then read(); -- found else -- match failed, skip line while peek() and read() ~= "\n" do end return nil; end end local tname = readTableName(); read("`"); read(" ") -- expect this if peek() == "(" then -- skip column list repeat until read() == ")"; read(" "); end for ch in ("VALUES "):gmatch(".") do read(ch); end -- expect this local tuples = readTuples(); read(";"); read("\n"); return tname, tuples; end local function readFile(filename) file = io.open(filename); if not file then error("File not found: "..filename); os.exit(0); end local t = {}; while true do local tname, tuples = readInsert(); if tname then if t[tname] then local t_name = t[tname]; for i=1,#tuples do table.insert(t_name, tuples[i]); end else t[tname] = tuples; end elseif peek() == nil then break; end end return t; end return readFile(filename); ------ end local arg, hostname = ...; local help = "/? -? ? /h -h /help -help --help"; if not(arg and hostname) or help:find(arg, 1, true) then print([[ejabberd SQL DB dump importer for Prosody Usage: ejabberdsql2prosody.lua filename.txt hostname The file can be generated using mysqldump: mysqldump db_name > filename.txt]]); os.exit(1); end local map = { ["last"] = {"username", "seconds", "state"}; ["privacy_default_list"] = {"username", "name"}; ["privacy_list"] = {"username", "name", "id"}; ["privacy_list_data"] = {"id", "t", "value", "action", "ord", "match_all", "match_iq", "match_message", "match_presence_in", "match_presence_out"}; ["private_storage"] = {"username", "namespace", "data"}; ["rostergroups"] = {"username", "jid", "grp"}; ["rosterusers"] = {"username", "jid", "nick", "subscription", "ask", "askmessage", "server", "subscribe", "type"}; ["spool"] = {"username", "xml", "seq"}; ["users"] = {"username", "password"}; ["vcard"] = {"username", "vcard"}; --["vcard_search"] = {}; } local NULL = {}; local parsed = parseFile(arg); for name, data in pairs(parsed) do local m = map[name]; if m then if #data > 0 and #data[1] ~= #m then print("[warning] expected "..#m.." columns for table `"..name.."`, found "..#data[1]); end for i=1,#data do local row = data[i]; for j=1,#m do row[m[j]] = row[j]; row[j] = nil; end end end end --print(serialize(t)); for _, row in ipairs(parsed["users"] or NULL) do local node, password = row.username, row.password; local ret, err = dm.store(node, hostname, "accounts", {password = password}); print("["..(err or "success").."] accounts: "..node.."@"..hostname); end function roster(node, host, jid, item) local roster = dm.load(node, host, "roster") or {}; roster[jid] = item; local ret, err = dm.store(node, host, "roster", roster); print("["..(err or "success").."] roster: " ..node.."@"..host.." - "..jid); end function roster_pending(node, host, jid) local roster = dm.load(node, host, "roster") or {}; roster.pending = roster.pending or {}; roster.pending[jid] = true; local ret, err = dm.store(node, host, "roster", roster); print("["..(err or "success").."] roster-pending: " ..node.."@"..host.." - "..jid); end function roster_group(node, host, jid, group) local roster = dm.load(node, host, "roster") or {}; local item = roster[jid]; if not item then print("Warning: No roster item "..jid.." for user "..node..", can't put in group "..group); return; end item.groups[group] = true; local ret, err = dm.store(node, host, "roster", roster); print("["..(err or "success").."] roster-group: " ..node.."@"..host.." - "..jid.." - "..group); end function private_storage(node, host, xmlns, stanza) local private = dm.load(node, host, "private") or {}; private[stanza.name..":"..xmlns] = st.preserialize(stanza); local ret, err = dm.store(node, host, "private", private); print("["..(err or "success").."] private: " ..node.."@"..host.." - "..xmlns); end function offline_msg(node, host, t, stanza) stanza.attr.stamp = os.date("!%Y-%m-%dT%H:%M:%SZ", t); stanza.attr.stamp_legacy = os.date("!%Y%m%dT%H:%M:%S", t); local ret, err = dm.list_append(node, host, "offline", st.preserialize(stanza)); print("["..(err or "success").."] offline: " ..node.."@"..host.." - "..os.date("!%Y-%m-%dT%H:%M:%SZ", t)); end for _, row in ipairs(parsed["rosterusers"] or NULL) do local node, contact = row.username, row.jid; local name = row.nick; if name == "" then name = nil; end local subscription = row.subscription; if subscription == "N" then subscription = "none" elseif subscription == "B" then subscription = "both" elseif subscription == "F" then subscription = "from" elseif subscription == "T" then subscription = "to" else error("Unknown subscription type: "..subscription) end; local ask = row.ask; if ask == "N" then ask = nil; elseif ask == "O" then ask = "subscribe"; elseif ask == "I" then roster_pending(node, hostname, contact); ask = nil; elseif ask == "B" then roster_pending(node, hostname, contact); ask = "subscribe"; else error("Unknown ask type: "..ask); end local item = {name = name, ask = ask, subscription = subscription, groups = {}}; roster(node, hostname, contact, item); end for _, row in ipairs(parsed["rostergroups"] or NULL) do roster_group(row.username, hostname, row.jid, row.grp); end for _, row in ipairs(parsed["vcard"] or NULL) do local stanza, err = parse_xml(row.vcard); if stanza then local ret, err = dm.store(row.username, hostname, "vcard", st.preserialize(stanza)); print("["..(err or "success").."] vCard: "..row.username.."@"..hostname); else print("[error] vCard XML parse failed: "..row.username.."@"..hostname); end end for _, row in ipairs(parsed["private_storage"] or NULL) do local stanza, err = parse_xml(row.data); if stanza then private_storage(row.username, hostname, row.namespace, stanza); else print("[error] Private XML parse failed: "..row.username.."@"..hostname); end end table.sort(parsed["spool"] or NULL, function(a,b) return a.seq < b.seq; end); -- sort by sequence number, just in case local time_offset = os.difftime(os.time(os.date("!*t")), os.time(os.date("*t"))) -- to deal with timezones local date_parse = function(s) local year, month, day, hour, min, sec = s:match("(....)-?(..)-?(..)T(..):(..):(..)"); return os.time({year=year, month=month, day=day, hour=hour, min=min, sec=sec-time_offset}); end for _, row in ipairs(parsed["spool"] or NULL) do local stanza, err = parse_xml(row.xml); if stanza then local last_child = stanza.tags[#stanza.tags]; if not last_child or last_child ~= stanza[#stanza] then error("Last child of offline message is not a tag"); end if last_child.name ~= "x" and last_child.attr.xmlns ~= "jabber:x:delay" then error("Last child of offline message is not a timestamp"); end stanza[#stanza], stanza.tags[#stanza.tags] = nil, nil; local t = date_parse(last_child.attr.stamp); offline_msg(row.username, hostname, t, stanza); else print("[error] Offline message XML parsing failed: "..row.username.."@"..hostname); end end