Compare commits

...

7 commits

Author SHA1 Message Date
mr❤️🤢
b784c963d2
Merge 3e44516095 into a3634c19cf 2025-06-26 13:20:29 +00:00
mrilyew
3e44516095 remove dumps 2025-06-26 16:20:19 +03:00
mrilyew
9079df9646 change pack() 2025-06-26 15:48:53 +03:00
mrilyew
636a9e5b67 draft v.0.99 2025-06-26 13:49:23 +03:00
mrilyew
c3b1e46909 draft no.0,33 2025-06-26 13:14:52 +03:00
ZAZiOs
a3634c19cf Деприватизация метода audio.get 2025-06-15 17:35:06 +03:00
mr❤️🤢
9714d0f036
feat(notes): use whitelist for images sources (#1352)
Переиначенный #942, но в нём картинка скачивалась с сервера, в этом в
конфиге задаётся список разрешённых хостов и затем идёт редирект если
ссылка прошла проверку. Если не прошла то редиректает на заглушку.
Впрочем, это не поможет если в конфиге не указан cdn, но по крайней мере
не будет приколов с автозапуском методов на основном сайте

После мержа в конфиг добавьте kaslana.ovk.to


![image](https://github.com/user-attachments/assets/6f72add7-1d9f-4ca8-8938-3e00be5b61f7)

---------

Co-authored-by: n1rwana <93197434+n1rwana@users.noreply.github.com>
2025-06-15 16:55:27 +03:00
16 changed files with 159 additions and 110 deletions

View file

@ -21,13 +21,7 @@ final class Audio extends VKAPIRequestHandler
$this->fail(201, "Access denied to audio(" . $audio->getId() . ")"); $this->fail(201, "Access denied to audio(" . $audio->getId() . ")");
} }
# рофлан ебало
$privApi = $hash && $GLOBALS["csrfCheck"];
$audioObj = $audio->toVkApiStruct($this->getUser()); $audioObj = $audio->toVkApiStruct($this->getUser());
if (!$privApi) {
$audioObj->manifest = false;
$audioObj->keys = false;
}
if ($need_user) { if ($need_user) {
$user = (new \openvk\Web\Models\Repositories\Users())->get($audio->getOwner()->getId()); $user = (new \openvk\Web\Models\Repositories\Users())->get($audio->getOwner()->getId());

View file

@ -620,10 +620,6 @@ final class Wall extends VKAPIRequestHandler
return (object) ["post_id" => $post->getVirtualId()]; return (object) ["post_id" => $post->getVirtualId()];
} }
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "wall.post", false)) {
$this->failTooOften();
}
$anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"]; $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"];
if ($wallOwner instanceof Club && $from_group == 1 && $signed != 1 && $anon) { if ($wallOwner instanceof Club && $from_group == 1 && $signed != 1 && $anon) {
$manager = $wallOwner->getManager($this->getUser()); $manager = $wallOwner->getManager($this->getUser());
@ -717,6 +713,10 @@ final class Wall extends VKAPIRequestHandler
$post->setSuggested(1); $post->setSuggested(1);
} }
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "wall.post")) {
$this->failTooOften();
}
$post->save(); $post->save();
} catch (\LogicException $ex) { } catch (\LogicException $ex) {
$this->fail(100, "One of the parameters specified was missing or invalid"); $this->fail(100, "One of the parameters specified was missing or invalid");
@ -730,8 +730,6 @@ final class Wall extends VKAPIRequestHandler
(new WallPostNotification($wallOwner, $post, $this->getUser()))->emit(); (new WallPostNotification($wallOwner, $post, $this->getUser()))->emit();
} }
\openvk\Web\Util\EventRateLimiter::i()->writeEvent("wall.post", $this->getUser(), $wallOwner);
return (object) ["post_id" => $post->getVirtualId()]; return (object) ["post_id" => $post->getVirtualId()];
} }

View file

@ -6,12 +6,42 @@ namespace openvk\Web\Models\Entities;
use HTMLPurifier_Config; use HTMLPurifier_Config;
use HTMLPurifier; use HTMLPurifier;
use HTMLPurifier_Filter;
class SecurityFilter extends HTMLPurifier_Filter
{
public function preFilter($html, $config, $context)
{
$html = preg_replace_callback(
'/<img[^>]*src\s*=\s*["\']([^"\']*)["\'][^>]*>/i',
function ($matches) {
$originalSrc = $matches[1];
$src = $originalSrc;
if (OPENVK_ROOT_CONF["openvk"]["preferences"]["notes"]["disableHotlinking"] ?? true) {
if (!str_contains($src, "/image.php?url=")) {
$src = '/image.php?url=' . base64_encode($originalSrc);
} /*else {
$src = preg_replace_callback('/(.*)\/image\.php\?url=(.*)/i', function ($matches) {
return base64_decode($matches[2]);
}, $src);
}*/
}
return str_replace($originalSrc, $src, $matches[0]);
},
$html
);
return $html;
}
}
class Note extends Postable class Note extends Postable
{ {
protected $tableName = "notes"; protected $tableName = "notes";
protected function renderHTML(): string protected function renderHTML(?string $content = null): string
{ {
$config = HTMLPurifier_Config::createDefault(); $config = HTMLPurifier_Config::createDefault();
$config->set("Attr.AllowedClasses", []); $config->set("Attr.AllowedClasses", []);
@ -78,8 +108,10 @@ class Note extends Postable
$config->set("Attr.AllowedClasses", [ $config->set("Attr.AllowedClasses", [
"underline", "underline",
]); ]);
$config->set('Filter.Custom', [new SecurityFilter()]);
$source = null; $source = $content;
if (!$source) {
if (is_null($this->getRecord())) { if (is_null($this->getRecord())) {
if (isset($this->changes["source"])) { if (isset($this->changes["source"])) {
$source = $this->changes["source"]; $source = $this->changes["source"];
@ -89,6 +121,7 @@ class Note extends Postable
} else { } else {
$source = $this->getRecord()->source; $source = $this->getRecord()->source;
} }
}
$purifier = new HTMLPurifier($config); $purifier = new HTMLPurifier($config);
return $purifier->purify($source); return $purifier->purify($source);
@ -117,7 +150,7 @@ class Note extends Postable
$this->save(); $this->save();
} }
return $cached; return $this->renderHTML($cached);
} }
public function getSource(): string public function getSource(): string

View file

@ -34,7 +34,6 @@ trait TSubscribable
"target" => $this->getId(), "target" => $this->getId(),
]; ];
$sub = $ctx->table("subscriptions")->where($data); $sub = $ctx->table("subscriptions")->where($data);
if (!($sub->fetch())) { if (!($sub->fetch())) {
$ctx->table("subscriptions")->insert($data); $ctx->table("subscriptions")->insert($data);

View file

@ -114,7 +114,6 @@ class User extends RowModel
public function getChandlerUser(): ChandlerUser public function getChandlerUser(): ChandlerUser
{ {
# TODO cache this function
return new ChandlerUser($this->getRecord()->ref("ChandlerUsers", "user")); return new ChandlerUser($this->getRecord()->ref("ChandlerUsers", "user"));
} }
@ -1740,41 +1739,51 @@ class User extends RowModel
return DatabaseConnection::i()->getContext()->table("blacklist_relations")->where("author", $this->getId())->count(); return DatabaseConnection::i()->getContext()->table("blacklist_relations")->where("author", $this->getId())->count();
} }
public function recieveEventsData(array $list): array public function getEventCounters(array $list): array
{ {
$count_of_keys = sizeof(array_keys($list));
$ev_str = $this->getRecord()->events_counters; $ev_str = $this->getRecord()->events_counters;
$values = []; $counters = [];
if (!$ev_str) { if (!$ev_str) {
for ($i = 0; $i < sizeof(array_keys($list)); $i++) { for ($i = 0; $i < sizeof(array_keys($list)); $i++) {
$values[] = 0; $counters[] = 0;
} }
} else { } else {
$keys = array_keys($list); $counters = unpack("S" . $count_of_keys, base64_decode($ev_str, true));
$values = unpack("S*", base64_decode($ev_str));
} }
return [ return [
'counters' => $values, 'counters' => array_combine(array_keys($list), $counters),
'refresh_time' => $this->getRecord()->events_refresh_time, 'refresh_time' => $this->getRecord()->events_refresh_time,
]; ];
} }
public function stateEvents(array $list): void public function stateEvents(array $state_list): void
{ {
$this->stateChanges("events_counters", base64_encode(pack("S*", array_values($list)))); $pack_str = "";
foreach ($state_list as $item => $id) {
$pack_str .= "S";
} }
public function resetEvents(array $list, int $restriction_length) $this->stateChanges("events_counters", base64_encode(pack($pack_str, ...array_values($state_list))));
if (!$this->getRecord()->events_refresh_time) {
$this->stateChanges("events_refresh_time", time());
}
}
public function resetEvents(array $list): void
{ {
$values = []; $values = [];
for ($i = 0; $i < sizeof(array_keys($list)); $i++) { foreach ($list as $key => $val) {
$values[] = 0; $values[$key] = 0;
} }
$this->stateEvents($values); $this->stateEvents($values);
$this->stateChanges("events_refresh_time", $restriction_length + time()); $this->stateChanges("events_refresh_time", time());
$this->save(); $this->save();
} }
} }

View file

@ -106,7 +106,7 @@ final class GiftsPresenter extends OpenVKPresenter
return; return;
} }
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "gifts.send", false)) { if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "gifts.send")) {
$this->flashFail("err", tr("error"), tr("limit_exceed_exception")); $this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
} }

View file

@ -63,15 +63,15 @@ final class GroupPresenter extends OpenVKPresenter
if ($_SERVER["REQUEST_METHOD"] === "POST") { if ($_SERVER["REQUEST_METHOD"] === "POST") {
if (!empty($this->postParam("name")) && mb_strlen(trim($this->postParam("name"))) > 0) { if (!empty($this->postParam("name")) && mb_strlen(trim($this->postParam("name"))) > 0) {
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "groups.create")) {
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
}
$club = new Club(); $club = new Club();
$club->setName($this->postParam("name")); $club->setName($this->postParam("name"));
$club->setAbout(empty($this->postParam("about")) ? null : $this->postParam("about")); $club->setAbout(empty($this->postParam("about")) ? null : $this->postParam("about"));
$club->setOwner($this->user->id); $club->setOwner($this->user->id);
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "groups.create")) {
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
}
try { try {
$club->save(); $club->save();
} catch (\PDOException $ex) { } catch (\PDOException $ex) {
@ -108,6 +108,12 @@ final class GroupPresenter extends OpenVKPresenter
$this->flashFail("err", tr("error"), tr("forbidden")); $this->flashFail("err", tr("error"), tr("forbidden"));
} }
if (!$club->getSubscriptionStatus($this->user->identity)) {
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "groups.sub")) {
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
}
}
$club->toggleSubscription($this->user->identity); $club->toggleSubscription($this->user->identity);
$this->redirect($club->getURL()); $this->redirect($club->getURL());

View file

@ -176,4 +176,26 @@ final class InternalAPIPresenter extends OpenVKPresenter
exit(''); exit('');
} }
} }
public function renderImageFilter()
{
$is_enabled = OPENVK_ROOT_CONF["openvk"]["preferences"]["notes"]["disableHotlinking"] ?? true;
$allowed_hosts = OPENVK_ROOT_CONF["openvk"]["preferences"]["notes"]["allowedHosts"] ?? [];
$url = $this->requestParam("url");
$url = base64_decode($url);
if (!$is_enabled) {
$this->redirect($url);
}
$url_parsed = parse_url($url);
$host = $url_parsed['host'];
if (in_array($host, $allowed_hosts)) {
$this->redirect($url);
} else {
$this->redirect('/assets/packages/static/openvk/img/fn_placeholder.jpg');
}
}
} }

View file

@ -418,6 +418,12 @@ final class UserPresenter extends OpenVKPresenter
if ($this->postParam("act") == "rej") { if ($this->postParam("act") == "rej") {
$user->changeFlags($this->user->identity, 0b10000000, true); $user->changeFlags($this->user->identity, 0b10000000, true);
} else { } else {
if ($user->getSubscriptionStatus($this->user->identity) == \openvk\Web\Models\Entities\User::SUBSCRIPTION_ABSENT) {
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "friends.outgoing_sub")) {
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
}
}
$user->toggleSubscription($this->user->identity); $user->toggleSubscription($this->user->identity);
} }

View file

@ -356,6 +356,10 @@ final class WallPresenter extends OpenVKPresenter
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
} }
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "wall.post")) {
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
}
$should_be_suggested = $wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2; $should_be_suggested = $wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2;
try { try {
$post = new Post(); $post = new Post();

View file

@ -385,8 +385,8 @@
<tr> <tr>
<td class="e"> <td class="e">
Vladimir Barinov (veselcraft), Celestora, Konstantin Kichulkin (kosfurler), Vladimir Barinov (veselcraft), Celestora, Konstantin Kichulkin (kosfurler),
Daniel Myslivets, Maxim Leshchenko (maksales / maksalees), n1rwana and Daniel Myslivets, Maxim Leshchenko (maksales / maksalees), n1rwana,
Jillian Österreich (Lumaeris) Jillian Österreich (Lumaeris) and MrIlyew (V00d00 M4g1c)
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -472,7 +472,7 @@
</tbody> </tbody>
</table> </table>
<table> {*<table>
<tbody> <tbody>
<tr class="h"> <tr class="h">
<th>OpenVK QA Team</th> <th>OpenVK QA Team</th>
@ -486,7 +486,7 @@
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>*}
<hr/> <hr/>

View file

@ -22,6 +22,8 @@
{if !is_null($thisUser) && !is_null($club ?? NULL) && $owner < 0} {if !is_null($thisUser) && !is_null($club ?? NULL) && $owner < 0}
{if $club->canBeModifiedBy($thisUser)} {if $club->canBeModifiedBy($thisUser)}
{var $anonHide = true}
<script> <script>
function onWallAsGroupClick(el) { function onWallAsGroupClick(el) {
document.querySelector("#forceSignOpt").style.display = el.checked ? "block" : "none"; document.querySelector("#forceSignOpt").style.display = el.checked ? "block" : "none";
@ -41,7 +43,7 @@
{/if} {/if}
{/if} {/if}
<label n:if="$anonEnabled" id="octoberAnonOpt" style="display: none;"> <label n:if="$anonEnabled" id="octoberAnonOpt" {if $anonHide}style="display: none;"{/if}>
<input type="checkbox" name="anon" /> {_as_anonymous} <input type="checkbox" name="anon" /> {_as_anonymous}
</label> </label>

View file

@ -13,19 +13,21 @@ class EventRateLimiter
use TSimpleSingleton; use TSimpleSingleton;
private $config; private $config;
private $availableFields;
public function __construct() public function __construct()
{ {
$this->config = OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"]; $this->config = OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"];
$this->availableFields = array_keys($this->config['list']);
} }
/* public function tryToLimit(?User $user, string $event_type, bool $is_update = true): bool
Checks count of actions for last hours {
Uses config path OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"] /*
Checks count of actions for last x seconds.
Uses OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"]
This check should be peformed only after checking other conditions cuz by default it increments counter
Returns: Returns:
@ -33,8 +35,7 @@ class EventRateLimiter
false the action can be performed false the action can be performed
*/ */
public function tryToLimit(?User $user, string $event_type, bool $distinct = true): bool
{
$isEnabled = $this->config['enable']; $isEnabled = $this->config['enable'];
$isIgnoreForAdmins = $this->config['ignoreForAdmins']; $isIgnoreForAdmins = $this->config['ignoreForAdmins'];
$restrictionTime = $this->config['restrictionTime']; $restrictionTime = $this->config['restrictionTime'];
@ -48,75 +49,45 @@ class EventRateLimiter
return false; return false;
} }
$eventsStats = $user->getEventCounters($eventsList);
$limitForThatEvent = $eventsList[$event_type]; $limitForThatEvent = $eventsList[$event_type];
$stat = $this->getEvent($event_type, $user);
bdump($stat);
$is_restrict_over = $stat["refresh_time"] < time() - $restrictionTime; $counters = $eventsStats["counters"];
$refresh_time = $eventsStats["refresh_time"];
$is_restrict_over = $refresh_time < (time() - $restrictionTime);
$event_counter = $counters[$event_type];
if ($is_restrict_over) { if ($refresh_time && $is_restrict_over) {
$user->resetEvents($eventsList, $restrictionTime); $user->resetEvents($eventsList);
return false; return false;
} }
$is = $stat["compared"] > $limitForThatEvent; $is_limit_exceed = $event_counter >= $limitForThatEvent;
if ($is === false) { if (!$is_limit_exceed && $is_update) {
$this->incrementEvent($event_type, $user); $this->incrementEvent($counters, $event_type, $user);
} }
return $is; return $is_limit_exceed;
} }
public function getEvent(string $event_type, User $by_user): array public function incrementEvent(array $old_values, string $event_type, User $initiator): bool
{ {
$ev_data = $by_user->recieveEventsData($this->config['list']);
$values = $ev_data['counters'];
$i = 0;
$compared = [];
bdump($values);
foreach ($this->config['list'] as $name => $value) {
bdump($value);
$compared[$name] = $values[$i];
$i += 1;
}
return [
"compared" => $compared,
"refresh_time" => $ev_data["refresh_time"]
];
}
/* /*
Updates counter for user Updates counter for user
*/ */
public function incrementEvent(string $event_type, User $initiator): bool
{
$isEnabled = $this->config['enable']; $isEnabled = $this->config['enable'];
$eventsList = OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"]; $eventsList = $this->config['list'];
if (!$isEnabled) { if (!$isEnabled) {
return false; return false;
} }
$ev_data = $initiator->recieveEventsData($eventsList); $old_values[$event_type] += 1;
$values = $ev_data['counters'];
$i = 0;
$compared = []; $initiator->stateEvents($old_values);
$initiator->save();
foreach ($eventsList as $name => $value) {
$compared[$name] = $values[$i];
$i += 1;
}
$compared[$event_type] += 1;
bdump($compared);
$initiator->stateEvents($compared);
return true; return true;
} }

View file

@ -413,6 +413,8 @@ routes:
handler: "InternalAPI->getPhotosFromPost" handler: "InternalAPI->getPhotosFromPost"
- url: "/iapi/getPostTemplate/{num}_{num}" - url: "/iapi/getPostTemplate/{num}_{num}"
handler: "InternalAPI->getPostTemplate" handler: "InternalAPI->getPostTemplate"
- url: "/image.php"
handler: "InternalAPI->imageFilter"
- url: "/tour" - url: "/tour"
handler: "About->tour" handler: "About->tour"
- url: "/fave" - url: "/fave"

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -50,7 +50,7 @@ openvk:
groups.sub: 50 groups.sub: 50
friends.outgoing_sub: 25 friends.outgoing_sub: 25
wall.post: 5000 wall.post: 5000
gifts.send: 20 gifts.send: 30
blacklists: blacklists:
limit: 100 limit: 100
applyToAdmins: true applyToAdmins: true
@ -70,6 +70,9 @@ openvk:
exposeOriginalURLs: true exposeOriginalURLs: true
newsfeed: newsfeed:
ignoredSourcesLimit: 50 ignoredSourcesLimit: 50
notes:
disableHotlinking: true
allowedHosts: []
wall: wall:
christian: false christian: false
anonymousPosting: anonymousPosting: