DARKNET: Prevent generating malformed darknet server hostname (#2744)

This commit is contained in:
catloversg
2026-05-10 06:18:19 +07:00
committed by GitHub
parent 0fe28a9fea
commit b79d5b1017
8 changed files with 79 additions and 5 deletions
+13 -4
View File
@@ -18,6 +18,7 @@ import { hasFullDarknetAccess } from "../effects/effects";
import { getFriendlyType, TypeAssertionError } from "../../utils/TypeAssertion";
import { isIPAddress } from "../../Types/strings";
import { roundToTwo } from "../../utils/helpers/roundToTwo";
import { safelyReverseString } from "../../utils/StringHelperFunctions";
export type PasswordResponse = {
code: DarknetResponseCode;
@@ -158,11 +159,11 @@ const decorateName = (name: string): string => {
const connector = connectors[Math.floor(Math.random() * connectors.length)];
if (Math.random() < 0.3) {
updatedName = l33tifyName(name);
updatedName = l33tifyName(updatedName);
}
if (Math.random() < 0.05) {
updatedName = updatedName.split("").reverse().join("");
updatedName = safelyReverseString(updatedName);
}
if (Math.random() < 0.1) {
@@ -180,7 +181,11 @@ const decorateName = (name: string): string => {
}
} while (GetServer(updatedName) !== null);
return updatedName;
// Defensive coding. All operations above preserve well-formed UTF-16, so this is currently redundant. It's a
// safeguard to ensure the function never returns ill-formed UTF-16 if future changes introduce code unitlevel
// manipulation.
// This normalization is lossy (lone surrogates -> U+FFFD).
return updatedName.toWellFormed();
};
const l33tifyName = (name: string): string => {
@@ -191,7 +196,11 @@ const l33tifyName = (name: string): string => {
const replacement: string = l33t[char] ?? "";
updatedName = updatedName.replaceAll(char, replacement);
}
return updatedName;
// Defensive coding. All operations above preserve well-formed UTF-16, so this is currently redundant. It's a
// safeguard to ensure the function never returns ill-formed UTF-16 if future changes introduce code unitlevel
// manipulation.
// This normalization is lossy (lone surrogates -> U+FFFD).
return updatedName.toWellFormed();
};
const getMaxRam = (difficulty: number): number => {
+6
View File
@@ -212,6 +212,12 @@ export class PlayerObject extends Person implements IPlayer {
delete player.jobs[loadedCompanyName as CompanyName];
}
}
// A bug created ill-formed UTF-16 darknet hostnames that caused the in-game editor to crash. Player.currentServer
// may point to one of these invalid hostnames. This code migrates the invalid hostnames and protects against
// similar issues in the future.
if (!player.currentServer.isWellFormed()) {
player.currentServer = player.currentServer.toWellFormed();
}
return player;
}
}
+19
View File
@@ -148,6 +148,25 @@ export function loadAllServers(saveString: string): void {
if (!(server instanceof Server) && !(server instanceof HacknetServer) && !(server instanceof DarknetServer)) {
throw new Error(`Server ${serverName} is not an instance of Server or HacknetServer or DarknetServer.`);
}
// Sanitize hostname
// A bug created ill-formed UTF-16 darknet hostnames that caused the in-game editor to crash. This code migrates
// those invalid hostnames and protects against similar issues in the future.
if (!server.hostname.isWellFormed()) {
server.hostname = server.hostname.toWellFormed();
for (const script of server.scripts.values()) {
script.server = server.hostname;
}
if (server.savedScripts) {
for (const script of server.savedScripts) {
script.server = server.hostname;
}
}
}
// Sanitize hostnames in server.serversOnNetwork
for (const [index, value] of server.serversOnNetwork.entries()) {
server.serversOnNetwork[index] = value.toWellFormed();
}
AllServers.set(server.hostname, server);
AllServers.set(server.ip, server);
}
+3 -1
View File
@@ -186,7 +186,9 @@ Copy your save here if possible
\`\`\`
`.trim();
const issueUrl = `${newIssueUrl}?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
const issueUrl = `${newIssueUrl}?title=${encodeURIComponent(title.toWellFormed())}&body=${encodeURIComponent(
body.toWellFormed(),
)}`;
return {
metadata,
+15
View File
@@ -105,3 +105,18 @@ export function getKeyFromReactElements(a: string | React.JSX.Element, b: string
const keyOfb = typeof b === "string" ? b : b.key ?? "";
return keyOfA + keyOfb;
}
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
/**
* input.split("") operates on UTF-16 code units and can break surrogate pairs.
* For example, 'a🅱️b' is 'a\uD83C\uDD71\uFE0Fb'. A naive reverse yields 'b\uFE0F\uDD71\uD83Ca', which is ill-formed
* UTF-16 and not 'b🅱️a' as expected.
* Passing such a string to encodeURIComponent may throw a URIError (e.g. in Monaco editor code when processing model
* ids).
*/
export function safelyReverseString(input: string): string {
return Array.from(graphemeSegmenter.segment(input), (s) => s.segment)
.reverse()
.join("");
}
+1
View File
@@ -780,6 +780,7 @@ describe("mutateDarknet and webstorm", () => {
function validatePath(hostname: string): void {
expectWithMessage(isDirectoryPath(`${hostname}/`), true, `Invalid hostname: ${hostname}`);
expectWithMessage(isFilePath(`${hostname}/data.txt`), true, `Invalid hostname: ${hostname}`);
expectWithMessage(hostname.isWellFormed(), true, `Malformed hostname: ${hostname}`);
}
describe("Darknet server name generator", () => {
+22
View File
@@ -6,6 +6,7 @@ import * as db from "../../../src/db";
import * as FileUtils from "../../../src/utils/FileUtils";
import type { SaveData } from "../../../src/types";
import { calculateExp } from "../../../src/PersonObjects/formulas/skill";
import { GetAllServers, GetServer } from "../../../src/Server/AllServers";
async function loadGameFromSaveData(saveData: SaveData) {
// Simulate loading the data in IndexedDB
@@ -132,4 +133,25 @@ describe("v3", () => {
expect(mockedDownload).not.toHaveBeenCalled();
});
});
test("Malformed hostname", async () => {
const saveData = new Uint8Array(fs.readFileSync("test/jest/Migration/save-files/malformed-hostname.gz"));
await loadGameFromSaveData(saveData);
for (const server of GetAllServers(true)) {
expect(server.hostname.isWellFormed()).toBe(true);
for (const script of server.scripts.values()) {
expect(script.server).toStrictEqual(server.hostname);
}
if (server.savedScripts) {
for (const script of server.savedScripts) {
expect(script.server).toStrictEqual(server.hostname);
}
}
for (const hostname of server.serversOnNetwork) {
expect(hostname.isWellFormed()).toBe(true);
expect(GetServer(hostname)).not.toBeNull();
}
}
expect(() => Player.getCurrentServer()).not.toThrow();
});
});
Binary file not shown.