Skip to content

Instantly share code, notes, and snippets.

@konstantin24121
Last active November 20, 2024 20:55
Show Gist options
  • Save konstantin24121/49da5d8023532d66cc4db1136435a885 to your computer and use it in GitHub Desktop.
Save konstantin24121/49da5d8023532d66cc4db1136435a885 to your computer and use it in GitHub Desktop.
Telegram Bot 6.0 Validating data received via the Web App node implementation
const TELEGRAM_BOT_TOKEN = '110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw'; // https://core.telegram.org/bots#creating-a-new-bot
export const verifyTelegramWebAppData = async (telegramInitData: string): boolean => {
// The data is a query string, which is composed of a series of field-value pairs.
const encoded = decodeURIComponent(telegramInitData);
// HMAC-SHA-256 signature of the bot's token with the constant string WebAppData used as a key.
const secret = crypto
.createHmac('sha256', 'WebAppData')
.update(TELEGRAM_BOT_TOKEN);
// Data-check-string is a chain of all received fields'.
const arr = encoded.split('&');
const hashIndex = arr.findIndex(str => str.startsWith('hash='));
const hash = arr.splice(hashIndex)[0].split('=')[1];
// sorted alphabetically
arr.sort((a, b) => a.localeCompare(b));
// in the format key=<value> with a line feed character ('\n', 0x0A) used as separator
// e.g., 'auth_date=<auth_date>\nquery_id=<query_id>\nuser=<user>
const dataCheckString = arr.join('\n');
// The hexadecimal representation of the HMAC-SHA-256 signature of the data-check-string with the secret key
const _hash = crypto
.createHmac('sha256', secret.digest())
.update(dataCheckString)
.digest('hex');
// if hash are equal the data may be used on your server.
// Complex data types are represented as JSON-serialized objects.
return _hash === hash;
};
@stasovlas
Copy link

Does anybody has got broken validation in your apps? I used this variant, but during this week it got broken.

Here's a variant for initDataUnsafe, which will create the right string for validation from the object and check the hash

const verifyDataIntegrity = (initDataUnsafe, hash) => {
        const dataCheckString = Object.entries(initDataUnsafe).sort().map(([k, v]) => {
            if (typeof v === "object" && v !== null) {
                v = JSON.stringify(v);
            }
            
            return `${k}=${v}`;
        }).join("\n");

        const secret = crypto.createHmac("sha256", "WebAppData").update(process.env.API_TOKEN ?? "");
        const calculatedHash = crypto.createHmac("sha256", secret.digest()).update(dataCheckString).digest("hex");
        
        return calculatedHash === hash;
};

Example of use

const { hash, ...rest } = window.Telegram.WebApp.initDataUnsafe;
console.log(verifyDataIntegrity(rest, hash));

its broken for me too

@painkkiller
Copy link

I had guess that initDataUnsafe has got new field(s), but so far I have no success in guessing how the hash is created

@nimaxin
Copy link

nimaxin commented Nov 17, 2024

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com.
To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

@stasovlas
Copy link

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com. To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

thank you! initDataUnsafe - is really unsafe =)

@painkkiller
Copy link

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com. To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

For me it isn't the case. I pass data as JSON in body of the POST query, and I don't see in prepared string any abnormalities.

@painkkiller
Copy link

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com. To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

thank you! initDataUnsafe - is really unsafe =)

How did you solve it? Could you show the code?

@nimaxin
Copy link

nimaxin commented Nov 17, 2024

@stasovlas
Copy link

stasovlas commented Nov 17, 2024

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com. To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

thank you! initDataUnsafe - is really unsafe =)

How did you solve it? Could you show the code?

just replace "/" in photo_url value by "/", like:

const dataCheckString = Object.entries(initDataUnsafe).sort().map(([k, v]) => {
           if (typeof v === "object" && v !== null) {
                if (k === "user") {
                    v = { ...v, photo_url: v.photo_url.replace("/", "\/") };
                }

                v = JSON.stringify(v);
            }
            
            return `${k}=${v}`;
        }).join("\n");

But better do not use initDataUnsafe

@painkkiller
Copy link

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com. To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

thank you! initDataUnsafe - is really unsafe =)

How did you solve it? Could you show the code?

just replace "/" in photo_url value by "/", like:

const dataCheckString = Object.entries(initDataUnsafe).sort().map(([k, v]) => {
           if (typeof v === "object" && v !== null) {
                v = JSON.stringify(v);
            }
            
            if(k === "photo_url") {
                return `${k}=${v.replace("/", "\/")}`;
            }
            
            return `${k}=${v}`;
        }).join("\n");

But better do not use initDataUnsafe

So from your code looks like initDataUnsafe is flatten? I use it as it is, and in my code user is a separate object inside initDataUnsafe

@painkkiller
Copy link

allows_write_to_pm=true
auth_date=XXXXXXXXXX
first_name=Dmitry
id=XXXXXXXXXX
language_code=ru
last_name=Malugin
photo_url=https://t.me/i/userpic/320/foto.svg
signature=XXXXXXXXX
username=PainKKKiller

This is the final string I am getting to be hashed, but it isn't working

@nimaxin
Copy link

nimaxin commented Nov 17, 2024

allows_write_to_pm=true
auth_date=XXXXXXXXXX
first_name=Dmitry
id=XXXXXXXXXX
language_code=ru
last_name=Malugin
photo_url=https://t.me/i/userpic/320/foto.svg
signature=XXXXXXXXX
username=PainKKKiller

This is the final string I am getting to be hashed, but it isn't working

replace / with \/
the final photo_url should look like this:
photo_url=https:\/\/t.me\/i\/userpic\/320\/foto.svg

@stasovlas
Copy link

stasovlas commented Nov 17, 2024

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com. To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

thank you! initDataUnsafe - is really unsafe =)

How did you solve it? Could you show the code?

just replace "/" in photo_url value by "/", like:

const dataCheckString = Object.entries(initDataUnsafe).sort().map(([k, v]) => {
           if (typeof v === "object" && v !== null) {
                v = JSON.stringify(v);
            }
            
            if(k === "photo_url") {
                return `${k}=${v.replace("/", "\/")}`;
            }
            
            return `${k}=${v}`;
        }).join("\n");

But better do not use initDataUnsafe

So from your code looks like initDataUnsafe is flatten? I use it as it is, and in my code user is a separate object inside initDataUnsafe

sorry, my mistake. I update code

@painkkiller
Copy link

initDataUnsafe

@stasovlas could you show your initDataUnsafe object? I still can't make it work (((

@brzhex
Copy link

brzhex commented Nov 17, 2024

Does anybody has got broken validation in your apps? I used this variant, but during this week it got broken.

Yes, after adding the photo_url parameter to initDataUnsafe, my code stopped working correctly. Here is its updated version:

const verifyDataIntegrity = (initDataUnsafe, hash) => {
        const dataCheckString = Object.entries(initDataUnsafe).sort().map(([k, v]) => {
            if (typeof v === "object" && v !== null) {
                v = JSON.stringify(v);
            }

            if (typeof v === "string" && /(https?:\/\/[^\s]+)/.test(v)) {
                v = v.replace(/\//g, "\\/");
            }
            
            return `${k}=${v}`;
        }).join("\n");

        const secret = crypto.createHmac("sha256", "WebAppData").update(process.env.API_TOKEN ?? "");
        const calculatedHash = crypto.createHmac("sha256", secret.digest()).update(dataCheckString).digest("hex");
        
        return calculatedHash === hash;
};

Example of use

const { hash, ...rest } = window.Telegram.WebApp.initDataUnsafe;
console.log(verifyDataIntegrity(rest, hash));

@painkkiller
Copy link

Object.entries(object).sort().map(([k, v]) => {
if (typeof v === "object" && v !== null) {
v = JSON.stringify(v);
}

        if (typeof v === "string" && /(https?:\/\/[^\s]+)/.test(v)) {
            v = v.replace(/\//g, "\\/");
        }

        return `${k}=${v}`;
    }).join("\n");

Thanks a lot! It work like a charm!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment