Mediawiki のスパムページと不正ユーザーを一括で削除するなど
Mediawikiに不正アクセスされて対処したのだが、なかなか面倒だったので、今後のためにメモ。
1.Mediawiki に何千人もの不正ユーザーが作成された。。。
FreeStyleWiki から Mediawikiに 移行作業をしている。自分がどこからでもメモを追加できるようにたてているWikiなので、
このあたりのページにあるように sysop にのみ edit権限を与えるつもりだったと思うのだが、
Mediawikiの勝手がわからなかったのか、以下の設定となっていた。
$wgGroupPermissions[‘*’][‘edit’] = false;
$wgGroupPermissions[‘user‘][‘edit’] = true;
このため、ユーザーの作成は自由 → ユーザーでログインすれば編集し放題な状態になっており、
以下のように辞書で組み合わせを作成したようなユーザーが1万件ほど作成されてしまっていた。
で、不正ユーザーでログインされて、スパムなページがこれまた大量に作成されてしまっていた。
2.対処する。
くそむかつくが、自らの手落ちなのであきらめて、以下の対応を順次とる。
- LocalSettingsで権限を厳格に【緊急】
- MySQLバックアップ【緊急】
- APIを用いてスパムページ削除【ぼちぼち】
- 不正ユーザーの削除【ぼちぼち】
2.1 LocalSettings で権限を厳格に
以下のURLを参考に、
https://www.mediawiki.org/wiki/Manual:User_rights/ja
また、権限群と、利用者グループ権限一覧を確認しながら、グルLocalSettings.phpを編集し、グループ権限をはがしていく。
$wgGroupPermissions['sysop']['edit'] = true; $wgGroupPermissions['sysop']['writeapi'] = true; $wgGroupPermissions['*']['createpage'] = false; $wgGroupPermissions['*']['edit'] = false; $wgGroupPermissions['*']['createaccount'] = false; $wgGroupPermissions['*']['writeapi'] = false; $wgGroupPermissions['*']['editmywatchlist'] = false; $wgGroupPermissions['*']['editmyoptions'] = false; $wgGroupPermissions['*']['createtalk'] = false; $wgGroupPermissions['*']['editmyprivateinfo'] = false; $wgGroupPermissions['*']['viewmyprivateinfo'] = false; $wgGroupPermissions['user']['edit'] = false; $wgGroupPermissions['user']['move-categorypages'] = false; $wgGroupPermissions['user']['purge'] = false; $wgGroupPermissions['user']['upload'] = false; $wgGroupPermissions['user']['movefile'] = false; $wgGroupPermissions['user']['createpage'] = false; $wgGroupPermissions['user']['editcontentmodel'] = false; $wgGroupPermissions['user']['move'] = false; $wgGroupPermissions['user']['move-subpages'] = false; $wgGroupPermissions['user']['sendemail'] = false; $wgGroupPermissions['user']['changetags'] = false; $wgGroupPermissions['user']['reupload-shared'] = false; $wgGroupPermissions['user']['move-rootuserpages'] = false; $wgGroupPermissions['user']['reupload'] = false; $wgGroupPermissions['user']['writeapi'] = false; $wgGroupPermissions['user']['minoredit'] = false; $wgGroupPermissions['user']['editmyusercss'] = false; $wgGroupPermissions['user']['editmyuserjson'] = false; $wgGroupPermissions['user']['editmyuserjs'] = false; $wgGroupPermissions['user']['editmyuserjsredirect'] = false; $wgGroupPermissions['user']['applychangetags'] = false; $wgGroupPermissions['user']['createtalk'] = false;
2.2 MySQLのバックアップ
最悪復元できるように、MySQLのバックアップを取っておく。
# mysqldump --default-character-set=utf8 -uroot -p --all-databases > mysqlbackup # tar -cvzf mysqlbackup.gz mysqlbackup
2.3 スパムページ削除と不正ユーザーの削除
ページの削除は、Mediawiki の APIを呼び出せばよいのだが、どうやら、Mediawikiではいったん作成されたユーザーは削除できないようだ。
なので、Extensionを導入して、削除したいユーザーを他のユーザーに統合、そのあとならユーザーを削除できるようだ。
https://www.mediawiki.org/wiki/Extension:UserMerge/ja
導入すると、削除するユーザーと統合先ユーザーを指定(削除する場合はチェックボックスをON)する画面が表示される。
APIを呼び出してページ削除と、Seleniumでブラウザを操作してユーザーの統合と削除の操作を行うやっつけ(使い捨て)C#プログラムを作成する。
以下をNuGetする
- Selenium.WebDriver
- Selenium.WebDriver.ChromeDriver
あと、Chrome とバージョンにあった、chromedriver_win32 をインストール(ダウンロードしてPATHを通す)が必要。
動かすと、こんな感じになる。
不正なユーザーに作成されたページを削除する。
以下のように呼び出す。
作りこむのが面倒だったので、Mediawiki API呼び出しのパラメータ(from,to,limit)を適当に変えながら、Visual Studioから実行。
var api = new MediaWikiApi(); // 有効なユーザーのリスト var validUser = new List() { "Piroto", "Xxx" }; api.DeleteInvalidUserCreatedPages(validUser);
同様に、不正なユーザーを削除する
var api = new MediaWikiApi(); // 有効な(削除しない)ユーザー var validUser = new List() { "Piroto", "Xxx" }; api.DeleteUsers("Xxx", validUser);
using System;
using System.Collections.Generic;
using System.Net.Http;
using Newtonsoft.Json;
using System.Text;
using System.IO;
using System.Net.Http.Headers;
using System.Web;
using System.Text.RegularExpressions;
using Fsw2Mw.Data;
using System.Linq;
using OpenQA.Selenium;
using OpenQA.Selenium.Edge;
using OpenQA.Selenium.Chrome;
using System.Threading;
namespace Fsw2Mw
{
class MediaWikiApi
{
static readonly string API_URL = @"https://yourmediawiki/api.php";
static readonly string USER_NAME = "yourid";
static readonly string PASSWORD = "yourpassword";
static readonly HttpClient client = new HttpClient();
public string Ready()
{
var loginToken = GetLoginToken();
LoginRequest(loginToken);
var csrfToken = CsrfToken();
return csrfToken;
}
/// <summary>
/// 指定されたユーザーが編集にかかわっていないページを削除する
/// https://www.mediawiki.org/wiki/API:Allpages
/// https://www.mediawiki.org/wiki/API:Delete
/// https://www.mediawiki.org/wiki/API:Properties
/// </summary>
public void DeleteInvalidUserCreatedPages(List<string> pValidUsers)
{
var csrfToken = Ready();
var validUsers = pValidUsers.Select((s) => s.ToLower());
var from = "N";
var to = from;
var limit = 5000;
string path = $@"c:\work\wiki_delete_info_from_{from}_to_{to}_limit_{limit}.csv";
using ( var writer = new StreamWriter(path, false, System.Text.Encoding.GetEncoding("shift_jis")))
{
writer.WriteLine("del,result,pageid,title,users");
var response = client.GetAsync($"{API_URL}?action=query&format=json&list=allpages&apfrom={from}&apTo={to}&aplimit={limit}").Result;
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
var json = JsonConvert.DeserializeObject<dynamic>(response.Content.ReadAsStringAsync().Result);
var pages = json["query"]["allpages"];
foreach(var page in pages)
{
var line = new StringBuilder();
Console.WriteLine($"{page.pageid}\t{page.title}");
line.Append($"\"{page.pageid}\",\"{page.title}\"");
var title = page.title;
var response2 = client.GetAsync($"{API_URL}?action=query&format=json&prop=contributors&titles={title}").Result;
var json2 = JsonConvert.DeserializeObject<dynamic>(response2.Content.ReadAsStringAsync().Result);
bool isDelete = true;
try
{
foreach(var delpage in json2.query.pages)
{
foreach(var editors in delpage)
{
foreach(var user in editors.contributors)
{
string username = user.name;
if (isDelete && validUsers.Contains(username.ToLower()))
{
isDelete = false;
}
line.Append($",{username}");
}
}
}
} catch(Exception)
{
isDelete = false;
}
var del = ((isDelete) ? "DEL" : "");
var delResult = "";
if (isDelete)
{
var param = new Dictionary<string, string>()
{
{"pageid", $"{page.pageid}" },
{"action","delete" },
{"format","json" },
{"token", csrfToken },
};
var content = new FormUrlEncodedContent(param);
var delRes = client.PostAsync($"{API_URL}", content).Result;
if (delRes.StatusCode == System.Net.HttpStatusCode.OK)
{
delResult = "DELETED";
try
{
var delContent = delRes.Content.ReadAsStringAsync().Result;
Console.WriteLine(delContent);
}
catch (Exception) { }
}
}
writer.WriteLine($"{del},{delResult}," + line.ToString());
}
}
}
}
/// <summary>
///
/// https://www.mediawiki.org/wiki/Extension:UserMerge/ja
/// </summary>
/// <param name="mergeUser"></param>
internal void DeleteUsers(string mergeUser, List<string> pValidUsers)
{
var deleteUsers = GetAllUsers(pValidUsers,500);
var driver = new ChromeDriver();
// ログインページ
driver.Navigate().GoToUrl("https://yourmediawiki/index.php?title=%E7%89%B9%E5%88%A5:%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3&returnto=%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8");
var loginUser = driver.FindElementByName("wpName");
loginUser.SendKeys(USER_NAME);
var passwd = driver.FindElementByName("wpPassword");
passwd.SendKeys(PASSWORD);
var loginButton = driver.FindElementByName("wploginattempt");
loginButton.Click();
Thread.Sleep(2000);
foreach (var deleteUser in deleteUsers)
{
// ユーザー統合と削除
driver.Navigate().GoToUrl("https://yourmediawiki/index.php/%E7%89%B9%E5%88%A5:%E5%88%A9%E7%94%A8%E8%80%85%E3%81%AE%E7%B5%B1%E5%90%88");
var spamUserText = driver.FindElementByName("wpolduser");
spamUserText.SendKeys(deleteUser);
var mergeUserText = driver.FindElementByName("wpnewuser");
mergeUserText.SendKeys(mergeUser);
var checkDelete = driver.FindElementByName("wpdelete");
checkDelete.Click();
mergeUserText.SendKeys(Keys.Enter);
Thread.Sleep(2000);
}
}
/// <summary>
/// https://www.mediawiki.org/wiki/API:Allusers
/// </summary>
public List<string> GetAllUsers(List<string> pExcludeUsers, int limit)
{
var result = new List<string>();
var excludeUsers = pExcludeUsers.Select((s) => s.ToLower());
var response = client.GetAsync($"{API_URL}?action=query&format=json&list=allusers&aulimit={limit}").Result;
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
var json = JsonConvert.DeserializeObject<dynamic>(response.Content.ReadAsStringAsync().Result);
foreach(var user in json.query.allusers)
{
var userName = $"{user.name}";
// 正常ユーザーは除外
if (!excludeUsers.Contains(userName.ToLower()))
{
result.Add(userName);
}
}
}
return result;
}
/// <summary>
/// 003.CSRFトークンの取得
/// </summary>
private string CsrfToken()
{
var param = new Dictionary<string, string>()
{
{ "action", "query" },
{"meta","tokens" },
{"format","json" },
};
var response = client.GetAsync($"{API_URL}?{new FormUrlEncodedContent(param).ReadAsStringAsync().Result}").Result;
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
var json = JsonConvert.DeserializeObject<dynamic>(response.Content.ReadAsStringAsync().Result);
var crsftoken = $"{json.query.tokens.csrftoken}";
Console.WriteLine($"CRSF TOKEN={crsftoken}");
return crsftoken;
}
else
{
throw new Exception("GetLoginToke Error");
}
}
/// <summary>
/// 002.ログイン
/// </summary>
/// <param name="loginToken"></param>
public void LoginRequest(string loginToken)
{
var param = new Dictionary<string, string>()
{
{"action", "login" },
{"lgname", USER_NAME},
{"lgpassword",PASSWORD },
{"lgtoken",loginToken },
{"format","json" },
};
var response = client.PostAsync(API_URL, new FormUrlEncodedContent(param)).Result;
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
var json = JsonConvert.DeserializeObject<dynamic>(response.Content.ReadAsStringAsync().Result);
Console.WriteLine(json);
}
else
{
throw new Exception("LoginRequest Error");
}
}
/// <summary>
/// 001.ログイントークンの取得
/// </summary>
public string GetLoginToken()
{
var param = new Dictionary<string, string>()
{
{"action", "query" },
{"meta","tokens" },
{"type","login" },
{"format","json" },
};
var response = client.GetAsync($"{API_URL}?{new FormUrlEncodedContent(param).ReadAsStringAsync().Result}").Result;
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
var json = JsonConvert.DeserializeObject<dynamic>(response.Content.ReadAsStringAsync().Result);
var logintoken = $"{json.query.tokens.logintoken}";
Console.WriteLine($"LOGIN TOKEN={logintoken}");
return logintoken;
}
else
{
throw new Exception("GetLoginToke Error");
}
}
}
}
