fix: ipc input validation - #2852
Conversation
- Validate IP/CIDR values from IPC before passing to Linux firewall - Replace shell interpolation with direct execve in firewall update functions - Block dangerous OpenVPN/WireGuard arguments in sanitizeArguments() - Add programId bounds check in IpcServerProcess::setProgram() - Add SO_PEERCRED peer authentication for IPC connections on Linux
cacf1d4 to
35acbad
Compare
| Tun2Socks, | ||
| CertUtil | ||
| CertUtil, | ||
| _Count |
There was a problem hiding this comment.
This exposes _Count to the whole ::amnezia namespace. Consider calling it something like PermittedProcessCount, or make PermittedProcess a enum class
There was a problem hiding this comment.
ok, this lines was changed to PermittedProcessCount like you advise
|
|
||
| switch (proc) { | ||
| case OpenVPN: { | ||
| static const QSet<QString> blocked = { |
There was a problem hiding this comment.
Blacklist strategy here is weaker than whitelisting IMO.
We have a very strict case of usage of OpenVPN, and we decide which args to call from the client application.
Let's put here only the fields which are actually used in client. It would be also useful to reuse the existing validation code
There was a problem hiding this comment.
switch to whitelisting strategy
| return out; | ||
| } | ||
| case Wireguard: { | ||
| static const QRegularExpression hookRe( |
There was a problem hiding this comment.
Same here. Use whitelisting strategy instead of blacklisting
| uid_t g_allowedUid = static_cast<uid_t>(-1); | ||
| bool g_allowedUidSet = false; | ||
|
|
||
| static bool checkPeerCredentials(QLocalSocket *socket) { |
There was a problem hiding this comment.
What's the point of this feature?
Theoretically, when the main app crashes - this thingy on the server keeps the previous UID. So it will not pass any other connections.
Client does not have any handles of restarting the service manually - so it will be stuck till manual restart or system reset
There was a problem hiding this comment.
in that condition ,scenario with different UIDs is practically impossible in real usage (the service runs as root, the client runs as the user), and a client crash does not change its UID on restart — the same user will reconnect with the same UID, so the service will not get stuck. But i agree that it can be discussed
There was a problem hiding this comment.
Ah, nevermind, I thought I saw pid here.
What's the point limiting multiple UIDs? I don't think that's a vector of attack IMO. Moreover, it uses the 1st one as "allowed", which opens a new exploits window - an attacker could try to connect to the IPC before the actual client.
There was a problem hiding this comment.
revert it, but it potencial risk which was describe in independed valnurable research
| return true; | ||
| } | ||
|
|
||
| static QStringList filterIpList(const QStringList &values) { |
There was a problem hiding this comment.
I think filtering-out invalid values is a bugprone way in here. Let's just print an error and return if any of them is not valid.
Modernize it: use std::all_of for this task, or similar
| static QStringList validateIpList(const QStringList &values) { | ||
| if (!std::all_of(values.cbegin(), values.cend(), isValidIpOrCidr)) { | ||
| qWarning() << "IPC: IP list contains invalid value, rejecting entire list"; | ||
| return {}; |
There was a problem hiding this comment.
That's weird reporting the warning which is actually critical, and do nothing in this case.
Why don't we check all the fields at the start of specific calls?
| // Whitelist only args actually used by the client: | ||
| // --config <path>, --management <host> <port>, --management-client | ||
| QStringList out; | ||
| for (int i = 0; i < args.size(); ++i) { |
There was a problem hiding this comment.
There is a mechanism of validation, which is already implemented. Why do we need the same thingy twice?
Check tun2socks implementation below
There was a problem hiding this comment.
reused existed code
|
|
||
| void IpcServerProcess::setProgram(int programId) | ||
| { | ||
| if (programId <= static_cast<int>(amnezia::PermittedProcess::Invalid) || |
There was a problem hiding this comment.
I think these checks are making the code to look complex without a reason, and they do nothing actually.
- If the error could happen returned - tell the caller about it. There is no point of logging the error without the actual error, program does not know what to do with it
- This call is used only by GUI application (and attackers maybe), so there is no check required IMO. You can also do asserts in here instead, to highlight an error for the developer
| } | ||
| case Wireguard: { | ||
| // Whitelist only subcommand + config file path (wg-quick up/down <conf>) | ||
| if (args.size() == 2) { |
There was a problem hiding this comment.
- Validator does not really care which args sequence does user provide - it's the problem of wg-quick itself. This is to be handled by wg-quick side.
- We do not use Wireguard in our app, and (I think) do not provide any execs of those names. Could be a vector of attack
So, I would recommend neither:
- To remove WireGuard out of here completely
- Or to use the same positional/named args in here as well as for Tun2Socks, for example
|
|
||
| switch (proc) { | ||
| case OpenVPN: { | ||
| // Whitelist only args actually used by the client: |
There was a problem hiding this comment.
Please keep the code clean from the slope comments. They do not provide any information - it is written below
| extern uid_t g_allowedUid; | ||
| extern bool g_allowedUidSet; | ||
|
|
||
| static bool checkPrivPeerCredentials(QLocalSocket *socket) { |
There was a problem hiding this comment.
Same question as for the WireGuard's localServer daemon. This feature is doubtful
| break; | ||
| } | ||
| dnsServers.append(dns.toString()); | ||
| const QString dnsStr = dns.toString(); |
There was a problem hiding this comment.
I would recommend to implement this check at the top of the method (basically, near the ranges check), so you would not repeat yourself for each OSes specifically
| QObject::connect(m_server.data(), &QLocalServer::newConnection, this, [this]() { | ||
| qDebug() << "LocalServer new connection"; | ||
| m_serverNode.addHostSideConnection(m_server->nextPendingConnection()); | ||
| QLocalSocket *conn = m_server->nextPendingConnection(); |
There was a problem hiding this comment.
What's the difference? Why do we change this
fix ipc input validation during test